Pergunta

Recentemente, descobri que, para alguns tipos de cálculos financeiros, o padrão a seguir é muito mais fácil de seguir e testar, especialmente em situações em que precisamos obter números de vários estágios do cálculo.

public class nonsensical_calculator
{ 

   ...

    double _rate;
    int _term;
    int _days;

    double monthlyRate { get { return _rate / 12; }}

    public double days { get { return (1 - i); }}
    double ar   { get { return (1+ days) /(monthlyRate  * days)
    double bleh { get { return Math.Pow(ar - days, _term)
    public double raar { get { return bleh * ar/2 * ar / days; }}
    ....
}

Obviamente, isso geralmente resulta em várias chamadas para o mesmo acessador dentro de uma determinada fórmula. Fiquei curioso para saber se o compilador é ou não o suficiente para otimizar essas chamadas repetidas, sem mudança intermediária no estado ou se esse estilo está causando um desempenho decente de desempenho.

Mais sugestões de leitura são sempre apreciadas

Foi útil?

Solução

Pelo que sei, o compilador C# não otimize isso, porque não pode ter certeza dos efeitos colaterais (por exemplo, e se você tiver accessCount++ no getter?) Dê uma olhada aqui em um Excelente resposta de Eric Lippert

Daquela resposta:

O compilador C# nunca faz esse tipo de otimização; Conforme observado, isso exigiria que o colega do compilador no código que está sendo chamado e verifique se o resultado que ele calcula não muda ao longo da vida útil do código do Callee. O compilador C# não o faz.

O compilador JIT pode. Não há razão para que não pudesse. Tem todo o código está bem ali. É totalmente gratuito para incluir a propriedade Getter e, se o jitter determinar que a propriedade Inlined Getter retorna um valor que pode ser armazenado em cache em um registro e reutilizado, é livre para fazê-lo. (Se você não deseja que isso faça isso porque o valor pode ser modificado em outro tópico, você já tem um bug de condição de corrida; corrige o bug antes de se preocupar com o desempenho.)

Apenas uma nota, visto que Eric está na equipe do compilador C#, confio na resposta dele :)

Outras dicas

Alguns pensamentos aleatórios.

Primeiro, como outros observaram, o compilador C# não faz esse tipo de otimização, embora o jitter seja livre para fazê -lo.

Segundo, a melhor maneira de responder a uma pergunta de desempenho é experimentá -la e ver. A aula de parada é sua amiga. Experimente um bilhão de vezes nos dois sentidos e veja qual é mais rápido; Então você saberá.

Terceiro, é claro que não faz sentido gastar tempo otimizando algo que já é rápido o suficiente. Antes de gastar muito tempo agindo, passe algum tempo perfilando e procurando pontos quentes. É improvável que isso seja um.

E quarto, outra resposta sugeriu o armazenamento de resultados intermediários em uma variável local. Observe que, em algumas situações, fazer isso pode tornar as coisas consideravelmente mais rápidas e, em outras, podem torná -lo mais lento. Às vezes, é mais rápido recomputar um resultado desnecessariamente do que armazená -lo e procurá -lo novamente quando precisar.

Como pode ser? Arquiteturas de chip com um pequeno número de registros - estou olhando para você, x86 - exigem que o jitter seja muito criterioso sobre quais habitantes locais estão em registros e que podem ser acessos de pilha. Incentivar o jitter a colocar algo que é usado com pouca frequência em um registro, às vezes significa forçar algo mais desse registro, algo que proporcionaria mais benefícios de estar em um registro do que o seu valor usado com pouca frequência.

Em resumo: não tente adivinhar o jitter da sua confortável poltrona; O comportamento do código do mundo real pode ser profundamente contra-intuitivo. Tome decisões de desempenho baseadas em medições empíricas realistas.

Certo, o compilador C# não faz otimizações como essa. Mas o compilador JIT certamente faz. Todos os getters que você postou são pequenos o suficiente para serem inlinados, resultando em um acesso direto ao campo.

Um exemplo:

static void Main(string[] args) {
  var calc = new nonsensical_calculator(42);
  double rate = calc.monthlyRate;
  Console.WriteLine(rate);
}

Gera:

00000000  push        ebp                          ; setup stack frame
00000001  mov         ebp,esp 
00000003  sub         esp,8 
00000006  mov         ecx,349DFCh                  ; eax = new nonsensical_calculator
0000000b  call        FFC50AD4 
00000010  fld         dword ptr ds:[006E1590h]     ; st0 = 42
00000016  fstp        qword ptr [eax+4]            ; _rate = st0
00000019  fld         qword ptr [eax+4]            ; st0 = _rate
0000001c  fdiv        dword ptr ds:[006E1598h]     ; st0 = st0 / 12
00000022  fstp        qword ptr [ebp-8]            ; rate = st0
      Console.WriteLine(rate);
// etc..

Observe como a chamada do construtor e a propriedade Getter desapareceram, eles estão inlinados no Main (). O código está acessando diretamente o campo _RATE. Até a variável de cálculo desapareceu, a referência é mantida no registro EAX.

A instrução no endereço 19 mostra que mais trabalho pode ser feito no otimizador. Permitir tempo.

Para dar uma rotação ligeiramente diferente, considere que as propriedades são realmente apenas embalagens em torno dos métodos assim que o código é compilado no IL. Então, se, em vez disso:

public class nonsensical_calculator
{
    double bleh
    {
        get { return Math.Pow(ar - days, _term); }
    }
    // etc.
}

Você tinha isso:

public class nonsensical_calculator
{
    double GetBleh()
    {
        return Math.Pow(ar - days, _term);
    }
}

Você esperaria que o compilador otimizasse o método chamado para você?

Eu não sou especialista no jitter, mas duvido que até o jitter "cache" isso; Teria que rastrear todos os tipos de estado e invalidar a entrada quando algum dos campos dependentes mudar e, por mais incrível que seja o jitter .NET, eu simplesmente não acho que seja tão inteligente. Pode incluir o método, mas isso geralmente não faz uma enorme diferença em termos de desempenho.

Resumindo, não confie no compilador ou no jitter para fazer essas otimizações para você. Além disso, você pode considerar seguir a diretriz de design comum de não colocar cálculos caros em getters de propriedades, porque parece Para o chamador ser barato, mesmo que não seja.

Se você precisar de desempenho, pré -computa esses valores sempre que os campos dependentes mudam. Ou, melhor ainda, perfil o código usando uma ferramenta como EQATEC (grátis) ou Formigas E veja se o custo do desempenho realmente está. Otimizar sem perfil é como atirar com a venda.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top