Essa é uma situação que encontro com frequência como um programador inexperiente e estou pensando sobre a qual, especialmente para um projeto meu ambicioso e que exige muita velocidade, estou tentando otimizar. Para as principais linguagens semelhantes a C (C, objC, C ++, Java, C #, etc) e seus compiladores usuais, essas duas funções serão executadas com a mesma eficiência? Existe alguma diferença no código compilado?
void foo1(bool flag)
{
if (flag)
{
//Do stuff
return;
}
//Do different stuff
}
void foo2(bool flag)
{
if (flag)
{
//Do stuff
}
else
{
//Do different stuff
}
}
Basicamente, existe um bônus / penalidade de eficiência direta ao break
iniciar ou return
iniciar o processo antes do tempo? Como o stackframe está envolvido? Existem casos especiais otimizados? Existem fatores (como inlining ou o tamanho de "Do stuff") que podem afetar isso significativamente?
Eu sempre sou um defensor de legibilidade melhorada em vez de otimizações menores (vejo muito foo1 com validação de parâmetro), mas isso surge com tanta frequência que eu gostaria de deixar de lado todas as preocupações de uma vez por todas.
E estou ciente das armadilhas da otimização prematura ... ugh, essas são algumas memórias dolorosas.
EDIT: Aceitei uma resposta, mas a resposta de EJP explica de forma bastante sucinta porque o uso de a return
é praticamente desprezível (em assembly, o return
cria um 'branch' para o final da função, que é extremamente rápido. O branch altera o registrador do PC e também pode afetar o cache e o pipeline, que são bem minúsculos.) Para este caso em particular, isso literalmente não faz diferença, porque tanto o if/else
quanto o return
criam a mesma ramificação até o final da função.
Respostas:
Não há diferença alguma:
O que significa nenhuma diferença no código gerado, mesmo sem otimização em dois compiladores
fonte
something()
sempre será executado. Na pergunta original, OP temDo stuff
eDo diffferent stuff
dependendo da bandeira. Não estou certo de que o código gerado será o mesmo.A resposta curta é: nenhuma diferença. Faça um favor a si mesmo e pare de se preocupar com isso. O compilador de otimização quase sempre é mais inteligente do que você.
Concentre-se na legibilidade e manutenção.
Se você quiser ver o que acontece, construa-os com otimizações e observe a saída do assembler.
fonte
x = <some number>
queif(<would've changed>) x = <some number>
ramos desnecessários podem realmente machucar. Por outro lado, a menos que isso esteja dentro do loop principal de uma operação extremamente intensiva, eu também não me preocuparia com isso.Respostas interessantes: Embora eu concorde com todas elas (até agora), há possíveis conotações para essa questão que até agora foram completamente desconsideradas.
Se o exemplo simples acima for estendido com a alocação de recursos e, em seguida, a verificação de erros com uma possível liberação de recursos resultante, o quadro pode mudar.
Considere a abordagem ingênua que os iniciantes podem adotar:
O que foi dito acima representaria uma versão extrema do estilo de retorno prematuro. Observe como o código se torna muito repetitivo e não pode ser mantido ao longo do tempo, quando sua complexidade aumenta. Hoje em dia, as pessoas podem usar o tratamento de exceções para capturá-los.
Philip sugeriu, após olhar o exemplo goto abaixo, usar um switch / case break-less dentro do bloco catch acima. Pode-se alternar (typeof (e)) e, em seguida, falhar nas
free_resourcex()
chamadas, mas isso não é trivial e precisa de consideração de design . E lembre-se de que um switch / case sem quebras é exatamente como o goto com rótulos encadeados abaixo ...Como Mark B apontou, em C ++ é considerado um bom estilo seguir o princípio de Aquisição de Recursos é Inicialização , RAII em resumo. A essência do conceito é usar a instanciação de objetos para adquirir recursos. Os recursos são então liberados automaticamente assim que os objetos saem do escopo e seus destruidores são chamados. Para recursos interdependentes, deve-se tomar cuidado especial para garantir a ordem correta de desalocação e projetar os tipos de objetos de forma que os dados necessários estejam disponíveis para todos os destruidores.
Ou em dias de pré-exceção pode fazer:
Mas este exemplo simplificado tem várias desvantagens: Ele pode ser usado apenas se os recursos alocados não dependerem uns dos outros (por exemplo, não poderia ser usado para alocar memória, abrir um filehandle e, em seguida, ler dados do manipulador para a memória ) e não fornece códigos de erro individiais e distinguíveis como valores de retorno.
Para manter o código rápido (!), Compacto e facilmente legível e extensível, Linus Torvalds impôs um estilo diferente para o código do kernel que lida com recursos, mesmo usando o infame goto de uma forma que faz sentido :
A essência da discussão nas listas de discussão do kernel é que a maioria dos recursos de linguagem que são "preferidos" sobre a instrução goto são gotos implícitos, como if / else enormes, semelhantes a árvore, manipuladores de exceção, instruções de loop / break / continue, etc. . E os goto's no exemplo acima são considerados ok, uma vez que estão saltando apenas uma pequena distância, têm rótulos claros e liberam o código de outras bagunças para acompanhar as condições de erro. Esta questão também foi discutida aqui no stackoverflow .
No entanto, o que está faltando no último exemplo é uma boa maneira de retornar um código de erro. Eu estava pensando em adicionar um
result_code++
após cadafree_resource_x()
chamada e retornar esse código, mas isso compensa alguns dos ganhos de velocidade do estilo de codificação acima. E é difícil retornar 0 em caso de sucesso. Talvez eu seja apenas sem imaginação ;-)Então, sim, eu acho que há uma grande diferença na questão de codificar retornos prematuros ou não. Mas também acho que é aparente apenas em códigos mais complicados, que são mais difíceis ou impossíveis de reestruturar e otimizar para o compilador. O que geralmente acontece quando a alocação de recursos entra em ação.
fonte
catch
contendo umaswitch
instrução sem interrupção no código de erro?Mesmo que isso não seja exatamente uma resposta, um compilador de produção será muito melhor em otimizar do que você. Eu favoreceria a legibilidade e manutenção sobre esses tipos de otimizações.
fonte
Para ser mais específico sobre isso, o
return
será compilado em uma ramificação ao final do método, onde haverá umaRET
instrução ou o que quer que seja. Se você deixá-lo de fora, o final do bloco antes doelse
será compilado em uma ramificação até o final doelse
bloco. Então você pode ver que neste caso específico não faz nenhuma diferença.fonte
Se você realmente deseja saber se há uma diferença no código compilado para seu compilador e sistema em particular, você terá que compilar e examinar o assembly por conta própria.
No entanto, no grande esquema das coisas, é quase certo que o compilador pode otimizar melhor do que o seu ajuste fino e, mesmo que não possa, é muito improvável que realmente importe para o desempenho do seu programa.
Em vez disso, escreva o código da maneira mais clara para humanos lerem e manterem, e deixe o compilador fazer o que ele faz de melhor: gerar o melhor assembly possível a partir de sua fonte.
fonte
Em seu exemplo, o retorno é perceptível. O que acontece com a pessoa que está depurando quando o retorno é uma ou duas páginas acima / abaixo, onde // coisas diferentes ocorrem? Muito mais difícil de encontrar / ver quando há mais código.
fonte
Eu concordo totalmente com o blueshift: legibilidade e manutenção primeiro !. Mas se você estiver realmente preocupado (ou apenas quiser saber o que seu compilador está fazendo, o que definitivamente é uma boa ideia no longo prazo), você deve procurar por si mesmo.
Isso significará usar um descompilador ou examinar a saída do compilador de baixo nível (por exemplo, idioma do assembly). Em C # ou em qualquer linguagem .Net, as ferramentas documentadas aqui fornecerão o que você precisa.
Mas, como você mesmo observou, essa provavelmente é uma otimização prematura.
fonte
From Clean Code: A Handbook of Agile Software Craftsmanship
no código fará com que o leitor navegue até a função e perca tempo lendo foo (sinalizador booleano)
Uma base de código melhor estruturada oferecerá melhores oportunidades para otimizar o código.
fonte
Uma escola de pensamento (não me lembro do egghead que o propôs no momento) é que todas as funções deveriam ter apenas um ponto de retorno de um ponto de vista estrutural para tornar o código mais fácil de ler e depurar. Isso, suponho, é mais para programar o debate religioso.
Um motivo técnico pelo qual você pode querer controlar quando e como uma função sai que quebra essa regra é quando você está codificando aplicativos em tempo real e deseja ter certeza de que todos os caminhos de controle através da função levam o mesmo número de ciclos de clock para serem concluídos.
fonte
Estou feliz que você trouxe esta questão. Você deve sempre usar os ramos em um retorno antecipado. Por que parar aí? Junte todas as suas funções em uma, se puder (pelo menos o máximo que puder). Isso é possível se não houver recursão. No final, você terá uma função principal massiva, mas é isso que você precisa / deseja para esse tipo de coisa. Depois, renomeie seus identificadores para serem o mais curtos possível. Dessa forma, quando seu código é executado, menos tempo é gasto lendo nomes. Em seguida faça ...
fonte