legibilidade versus código mais curto ao retornar da função [closed]

4

Em algo tão simples como

int sq(int x) { int y = x*x; return y; }

versus

int sq(int x) { return (x*x); }

a função anterior requer um passo extra de IL.

EDIT: código IL do primeiro exemplo, usando o compilador C # atual do VS 2013 Update 4 (.NET Framework 4.5.1), código da versão otimizada:

  IL_0000:  ldarg.0
  IL_0001:  ldarg.0
  IL_0002:  mul
  IL_0003:  stloc.0   // These two instructions
  IL_0004:  ldloc.0   // seem to be unneccessary
  IL_0005:  ret

e do segundo exemplo:

  IL_0000:  ldarg.0
  IL_0001:  ldarg.0
  IL_0002:  mul
  IL_0003:  ret

Esse passo extra IL é indicativo de dados sendo copiados na memória? Essa diferença ainda está presente quando se usa algo aparentemente melhor otimizado? (C?)

Se sim para o primeiro ou ambos: posso usar o último, especialmente quando os dados retornados ocupam muito espaço e, presumivelmente, muito tempo para serem movidos na memória? Ou devo favorecer o primeiro, porque é mais legível? O LINQ facilita ter todas as funções dentro de uma longa linha de retorno (), mas meu exemplo tem a mesma escolha, seja C. Velocidade ou legibilidade?

Em geral, você colocaria o último bit de matemática entre os parênteses na linha de retorno?

    
por Nick Alexander 21.12.2014 / 03:57
fonte

4 respostas

13

Primeiro, micro-otimizações como essa raramente fazem sentido. Você deve se concentrar na capacidade de leitura e se preocupar com esses tipos de otimização apenas quando tiver identificado o código que realmente precisa otimizar criando o perfil.

Agora, se você ainda está interessado em performance, olhar para o IL não faz muito sentido, porque não é o IL que é executado.

Em vez disso, o que você deve fazer é medir .

Outra opção seria o código de máquina gerado (geralmente x86). Mas o problema é que as CPUs são muito complicadas e pode ser muito difícil descobrir qual versão do código será mais rápida.

Nota: olhar o código da máquina não é tão simples quanto abrir a janela Desmontagem. Você precisa rodar no modo Release e certificar-se de que o CLR realmente otimize o código (seja desmarcando "Supressar otimização JIT no carregamento do módulo" ou iniciando sem o depurador anexado: Ctrl + F5 , e anexando-o posteriormente, por exemplo, usando Debugger.Break() e selecionando para depurar o código).

Se você fizer isso, provavelmente notará que ambas as versões do método serão inlined, o que torna difícil ver quais instruções pertencem ao seu método (mas ambas as versões geram o mesmo código de máquina para mim).

Se você quiser comparar as duas versões do método isoladamente, use [MethodImpl(MethodImplOptions.NoInlining)] . Com isso, o código que estou recebendo para as duas versões do método é o mesmo:

push        ebp  
mov         ebp,esp  
mov         eax,edx  
imul        eax,edx  
pop         ebp  
ret  
    
por 21.12.2014 / 05:21
fonte
16

Qualquer compilador independente compilará os dois exemplos para o mesmo IL. Além disso, o primeiro não é mais legível, já que você tem uma variável de lixo sem informações. É apenas o ruído que diminui a legibilidade. Se esse temporário realmente tivesse um bom nome que fornecesse alguma informação útil para o leitor, então o primeiro caminho (normalmente) seria melhor.

Mas, em geral, prefere a legibilidade à velocidade (que é apenas mal correlacionada ao tamanho do código).

    
por 21.12.2014 / 04:09
fonte
4

O IL que você está vendo não é necessariamente indicativo do código nativo que o compilador JIT gerará em tempo de execução (ou que o ngen irá gerar se você pré-compilar seus binários nativos).

A IL, neste caso, obviamente permanece um pouco representativa do código original.

EDITAR: Os comentários postados depois que o OP postou o IL e a desmontagem notaram que o assembly é uma compilação de depuração, portanto as otimizações estão desativadas e o JIT provavelmente otimizará isso se você compilar uma compilação de lançamento.

EDIT: E eu definitivamente concordo com os comentários que a depuração da variante "atribuir à variável, retornar a variável" é mais fácil, pelo simples fato de que você pode inspecionar o resultado da tarefa antes do método retorna.

No entanto, cada compilador (C #, VB.NET, FORTRAN, COBOL, F #, etc.) é um animal diferente, transformando essas linguagens em IL comum, que é então transformada pelo JIT em código nativo para a CPU do CLR. encontra-se em execução.

O IL é um acrônimo para "Intermediate Language", afinal de contas, não para "Linguagem de Máquinas Nativas Totalmente Otimizada". Eu imagino que o retorno sobre o investimento da criação de IL totalmente otimizada quando o próprio JIT já é um compilador otimizador pode não valer a pena na maioria dos casos.

Além disso, pode ser interessante se você postar o IL real de cada exemplo.

Acho que não me preocuparia com isso.

Provavelmente é uma ocorrência rara que você vai superar as otimizações incorporadas no compilador. Essas otimizações são o que esses caras (e / ou garotas) estão focando o tempo todo.

Eu me preocupo mais com código correto, legível por humanos, fácil de depurar, e a regra 80/20. 80% dos problemas de desempenho estarão em 20% do código. Na verdade, é provavelmente mais como 95/5.

Faça o perfil do seu código. Execute testes e cronometre-os, depois otimize as partes que são realmente problemáticas. Eu aposto que você almoça que os bits problemáticos geralmente estão em lugares que você não espera, e muitos códigos que você achava que seriam um problema de desempenho não serão, porque eles são executados raramente ou não estão em um caminho supercrítico. E aposto que o JIT otimiza tudo o que você está preocupado com este exemplo, de qualquer maneira. ; -)

    
por 21.12.2014 / 08:26
fonte
1

Vou desafiar diretamente que o primeiro exemplo é mais legível. Neste código, é muito claro o que está acontecendo, você pega um int x e retorna x*x .

int sq(int x) {
    return (x*x);
}

Adicionar uma instrução extra ( y=x*x; return y ) não adiciona nada à legibilidade e, de fato, me faz pensar sobre o que posso estar perdendo (por exemplo, havia linhas que você removeu) porque está atribuindo uma nova variável. p>

No entanto, no argumento geral "legibilidade versus velocidade", sugiro que a legibilidade supere todas as outras. Na maioria dos casos, o tempo do programador é muito mais caro do que computação ou armazenamento. CodeGolf.StackExchange é um exemplo exemplar disso, já que é preenchido com muito de soluções muito rápidas e muito inteligentes para os problemas, mas o tempo para realmente entender como algumas delas funcionam é bastante longo.

"Bottlenecks occur in surprising places, so don't try to second guess and put in a speed hack until you have proven that's where the bottleneck is." — Rob Pike

As otimizações no código para melhorar o desempenho do código de bytes devem ser consideradas apenas depois que todas as outras partes do código tiverem um perfil para identificar possíveis gargalos . Se a otimização do bytecode fizer uma operação O(n!) ingênua um pouco mais rápida para cada n , encontrar uma solução O(n^2) no código concederá resultados muito melhores.

Mesmo assim, quando você começa a investigar a operação do bytecode para aumentar o desempenho extra, o custo de adicionar um núcleo extra a um servidor ou comprar um pouco mais de tempo do servidor pode ser minúsculo comparado ao tempo necessário para alterações de código e investigações. uma solução ótima de bytecode - a razão pela qual digo isso é que, se uma solução ótima de bytecode fosse trivialmente fácil, ela já seria incorporada ao compilador / intérprete.

    
por 22.12.2014 / 04:16
fonte

Tags