Eu sou uma pessoa religiosa e faço esforços para não cometer pecados. É por isso que costumo escrever funções pequenas ( menores que isso , para reformular Robert C. Martin) para cumprir os vários mandamentos ordenados pela Bíblia do Código Limpo . Mas enquanto checava algumas coisas, cheguei neste post , abaixo do qual li este comentário:
Lembre-se de que o custo de uma chamada de método pode ser significativo, dependendo do idioma. Quase sempre há uma troca entre escrever código legível e escrever código de desempenho.
Em que condições essa declaração citada ainda é válida hoje em dia, dada a rica indústria de compiladores modernos com bom desempenho?
Essa é a minha única pergunta. E não se trata de escrever funções longas ou pequenas. Apenas enfatizo que seu feedback pode ou não contribuir para alterar minha atitude e me deixa incapaz de resistir à tentação dos blasfemadores .
fonte
for(Integer index = 0, size = someList.size(); index < size; index++)
vez de simplesmentefor(Integer index = 0; index < someList.size(); index++)
. Só porque o seu compilador foi criado nos últimos anos não significa necessariamente que você pode renunciar à criação de perfil.main()
, outros dividem tudo em 50 pequenas funções e todos são totalmente ilegíveis. O truque é, como sempre, encontrar um bom equilíbrio .Respostas:
Depende do seu domínio.
Se você estiver escrevendo um código para um microcontrolador de baixa potência, o custo da chamada do método pode ser significativo. Mas se você estiver criando um site ou aplicativo normal, o custo da chamada de método será insignificante em comparação com o restante do código. Nesse caso, sempre vale mais a pena focar nos algoritmos e estruturas de dados corretos, em vez de micro-otimizações, como chamadas de método.
E também há a questão do compilador incluir os métodos para você. A maioria dos compiladores é inteligente o suficiente para incorporar funções onde for possível.
E por último, há uma regra de ouro do desempenho: SEMPRE PERFIL PRIMEIRO. Não escreva código "otimizado" com base em suposições. Se você não estiver em uso, escreva os dois casos e veja qual é o melhor.
fonte
A sobrecarga da chamada de função depende inteiramente do idioma e em que nível você está otimizando.
Em um nível ultra baixo, as chamadas de função e ainda mais as chamadas de método virtual podem ser caras se levarem a erros de previsão de ramificação ou falhas no cache da CPU. Se você escreveu o assembler , também saberá que precisa de algumas instruções extras para salvar e restaurar registros em torno de uma chamada. Não é verdade que um compilador "suficientemente inteligente" seria capaz de incorporar as funções corretas para evitar essa sobrecarga, porque os compiladores são limitados pela semântica da linguagem (especialmente em torno de recursos como envio de método de interface ou bibliotecas carregadas dinamicamente).
Em um nível alto, linguagens como Perl, Python, Ruby fazem muita contabilidade por chamada de função, tornando-as comparativamente caras. Isso é agravado pela metaprogramação. Uma vez eu acelerei um software Python 3x apenas ao elevar as chamadas de função de um loop muito quente. No código crítico de desempenho, as funções auxiliares embutidas podem ter um efeito perceptível.
Mas a grande maioria dos softwares não é tão crítica em termos de desempenho que você seria capaz de perceber as despesas gerais das chamadas de função. De qualquer forma, escrever um código simples e limpo compensa:
Se o seu código não é crítico para o desempenho, isso facilita a manutenção. Mesmo em softwares críticos para o desempenho, a maioria do código não será um "ponto de acesso".
Se o seu código é crítico para o desempenho, o código simples facilita a compreensão do código e identifica oportunidades de otimização. As maiores vitórias geralmente não vêm de micro-otimizações, como funções embutidas, mas de melhorias algorítmicas. Ou formulado de maneira diferente: não faça a mesma coisa mais rápido. Encontre uma maneira de fazer menos.
Observe que "código simples" não significa "fatorado em mil pequenas funções". Toda função também introduz um pouco de sobrecarga cognitiva - é mais difícil argumentar sobre um código mais abstrato. Em algum momento, essas pequenas funções podem fazer tão pouco que não usá-las simplificaria seu código.
fonte
Quase todos os ditados sobre o ajuste do código para desempenho são casos especiais da lei de Amdahl . A declaração curta e bem-humorada da lei de Amdahl é
(Otimizar tudo para zero por cento do tempo de execução é totalmente possível: quando você se senta para otimizar um programa grande e complicado, é provável que descubra que ele está gastando pelo menos parte do tempo de execução em coisas que não precisa fazer nada .)
É por isso que as pessoas normalmente dizem que não se preocupam com os custos das chamadas de funções: não importa o quanto sejam caras, normalmente o programa como um todo gasta apenas uma pequena fração de seu tempo de execução em sobrecarga de chamadas, portanto, acelerá-los não ajuda muito .
Mas, se houver um truque que você possa executar que torne todas as chamadas de função mais rápidas, esse truque provavelmente valerá a pena. Os desenvolvedores de compiladores gastam muito tempo otimizando a função "prólogos" e "epílogos", porque isso beneficia todos os programas compilados com esse compilador, mesmo que seja apenas um pouquinho para cada um.
E, se você tiver motivos para acreditar que um programa está gastando muito tempo de execução apenas fazendo chamadas de função, comece a pensar se algumas dessas chamadas de função são desnecessárias. Aqui estão algumas regras práticas para saber quando você deve fazer isso:
Se o tempo de execução por invocação de uma função for menor que um milissegundo, mas essa função for chamada centenas de milhares de vezes, provavelmente deverá ser incorporada.
Se um perfil do programa mostra milhares de funções e nenhuma delas ocupa mais de 0,1% ou mais do tempo de execução, a sobrecarga de chamada de função provavelmente é significativa em termos agregados.
Se você tiver " código de lasanha " , no qual existem muitas camadas de abstração que dificilmente funcionam além do envio para a próxima camada, e todas essas camadas são implementadas com chamadas de método virtual, há uma boa chance de a CPU estar desperdiçando um muito tempo em barracas de tubulação de ramificação indireta. Infelizmente, a única cura para isso é livrar-se de algumas camadas, o que geralmente é muito difícil.
fonte
final
classes e métodos onde aplicável em Java, ou nãovirtual
métodos em C # ou C ++), o indireto pode ser eliminado pelo compilador / tempo de execução e você ' Veremos um ganho sem reestruturação maciça. Como aponta @JorgWMittag acima, a JVM pode até mesmo embutido nos casos em que não é provável que a otimização é ...Vou desafiar esta citação:
Esta é uma afirmação realmente enganosa e uma atitude potencialmente perigosa. Existem alguns casos específicos em que você precisa fazer uma troca, mas, em geral, os dois fatores são independentes.
Um exemplo de uma troca necessária é quando você tem um algoritmo simples versus um mais complexo, mas com mais desempenho. Uma implementação de hashtable é claramente mais complexa do que uma implementação de lista vinculada, mas a pesquisa será mais lenta, portanto, você pode precisar trocar a simplicidade (que é um fator de legibilidade) para o desempenho.
Com relação à sobrecarga de chamada de função, transformar um algoritmo recursivo em uma iterativa pode ter um benefício significativo, dependendo do algoritmo e do idioma. Mas esse é novamente um cenário muito específico e, em geral, a sobrecarga das chamadas de função será desprezível ou otimizada.
(Algumas linguagens dinâmicas como o Python têm uma sobrecarga significativa de chamada de método. Mas se o desempenho se tornar um problema, você provavelmente não deveria estar usando o Python em primeiro lugar.)
A maioria dos princípios para código legível - formatação consistente, nomes de identificadores significativos, comentários apropriados e úteis e assim por diante não afetam o desempenho. E alguns - como usar enums em vez de strings - também têm benefícios de desempenho.
fonte
A sobrecarga da chamada de função não é importante na maioria dos casos.
No entanto, o maior ganho do código embutido é otimizar o novo código após a inclusão .
Por exemplo, se você chamar uma função com um argumento constante, o otimizador agora poderá dobrar esse argumento constantemente onde não podia antes de incluir a chamada. Se o argumento for um ponteiro de função (ou lambda), o otimizador também poderá incorporar as chamadas para esse lambda.
Esse é um grande motivo pelo qual as funções virtuais e os ponteiros de função não são atraentes, pois você não pode incorporá-los, a menos que o ponteiro de função real tenha sido constantemente dobrado até o site da chamada.
fonte
Assumindo que o desempenho é importante para o seu programa e, de fato, tem muitas e muitas chamadas, o custo ainda pode ou não ser importante, dependendo do tipo de chamada.
Se a função chamada for pequena e o compilador puder incorporá-la, o custo será essencialmente zero. As implementações modernas de compiladores / idiomas têm JIT, otimizações de tempo de link e / ou sistemas de módulos projetados para maximizar a capacidade de incorporar funções quando for benéfico.
OTOH, há um custo não óbvio para chamadas de função: sua mera existência pode inibir otimizações do compilador antes e depois da chamada.
Se o compilador não puder raciocinar sobre o que a função chamada faz (por exemplo, é despacho virtual / dinâmico ou uma função em uma biblioteca dinâmica), pode ser necessário supor pessimisticamente que a função possa ter algum efeito colateral - lance uma exceção, modifique estado global ou altere qualquer memória vista através de ponteiros. O compilador pode precisar salvar valores temporários na memória traseira e lê-los novamente após a chamada. Ele não poderá reordenar as instruções em torno da chamada, portanto, poderá não conseguir vetorizar loops ou extrair computação redundante dos loops.
Por exemplo, se você chamar desnecessariamente uma função em cada iteração de loop:
O compilador pode saber que é uma função pura e movê-la para fora do loop (em um caso terrível como este exemplo até corrige o algoritmo acidental O (n ^ 2) como O (n)):
E então, talvez até reescreva o loop para processar elementos 4/8/16 de cada vez usando instruções gerais / SIMD.
Mas se você adicionar uma chamada a algum código opaco no loop, mesmo que a chamada não faça nada e seja super barata, o compilador deve assumir o pior - que a chamada acessará uma variável global que aponta para a mesma memória que a
s
alteração seu conteúdo (mesmo que estejaconst
em sua função, pode não estar emconst
nenhum outro lugar), impossibilitando a otimização:fonte
Este artigo antigo pode responder à sua pergunta:
Abstrato:
fonte
No C ++, cuidado com o design de chamadas de função que copiam argumentos, o padrão é "passar por valor". A sobrecarga da chamada de função devido ao salvamento de registros e outras coisas relacionadas ao quadro da pilha pode ser sobrecarregada por uma cópia não intencional (e potencialmente muito cara) de um objeto.
Há otimizações relacionadas ao quadro de pilha que você deve investigar antes de desistir de código altamente fatorado.
Na maioria das vezes, quando tive que lidar com um programa lento, descobri que fazer alterações algorítmicas produzia acelerações muito maiores do que as chamadas de função embutidas. Por exemplo: outro engenheiro refez um analisador que preencheu uma estrutura de mapa de mapas. Como parte disso, ele removeu um índice em cache de um mapa para um associado logicamente. Essa foi uma boa jogada de robustez do código, no entanto, tornou o programa inutilizável devido a um fator de desaceleração de 100 devido à realização de uma pesquisa de hash para todos os acessos futuros em comparação ao uso do índice armazenado. A criação de perfil mostrou que a maior parte do tempo era gasta na função de hash.
fonte
Sim, uma previsão de ramificação perdida é mais cara no hardware moderno do que há décadas atrás, mas os compiladores ficaram muito mais inteligentes ao otimizar isso.
Como exemplo, considere Java. À primeira vista, a sobrecarga de chamada de função deve ser particularmente dominante neste idioma:
Horrorizado com essas práticas, o programador médio de C preveria que o Java deveria ser pelo menos uma ordem de magnitude mais lenta que C. E há 20 anos, ele estaria certo. No entanto, os benchmarks modernos colocam o código Java idiomático dentro de alguns por cento do código C equivalente. Como isso é possível?
Uma razão é que a função embutida das JVMs modernas chama normalmente. Faz isso usando inlining especulativo:
Ou seja, o código:
é reescrito para
E, é claro, o tempo de execução é inteligente o suficiente para passar para cima nessa verificação de tipo, desde que o ponto não seja atribuído, ou excluí-la se o tipo for conhecido pelo código de chamada.
Em resumo, se até o Java gerencia a inserção automática de métodos, não há razão inerente para que um compilador não suporte a inserção automática, e todos os motivos para fazê-lo, porque a inserção é altamente benéfica para os processadores modernos. Portanto, dificilmente posso imaginar qualquer compilador convencional moderno ignorando essas estratégias básicas de otimização e presumiria um compilador capaz disso, a menos que provado o contrário.
fonte
Como outros dizem, você deve medir o desempenho do seu programa primeiro e provavelmente não encontrará nenhuma diferença na prática.
Ainda assim, do nível conceitual, pensei em esclarecer algumas coisas que estão conflitantes em sua pergunta. Em primeiro lugar, você pergunta:
Observe as palavras-chave "função" e "compiladores". Sua cotação é sutilmente diferente:
Trata-se de métodos , no sentido orientado a objetos.
Enquanto "função" e "método" são frequentemente usados de forma intercambiável, existem diferenças no que diz respeito ao custo (do que você está perguntando) e quando se trata de compilação (que é o contexto que você forneceu).
Em particular, precisamos saber sobre despacho estático versus despacho dinâmico . Ignorarei otimizações no momento.
Em uma linguagem como C, geralmente chamamos funções com despacho estático . Por exemplo:
Quando o compilador vê a chamada
foo(y)
, ele sabe a qual função essefoo
nome está se referindo, para que o programa de saída possa ir direto para afoo
função, o que é bastante barato. É isso que despacho estático significa.A alternativa é o envio dinâmico , onde o compilador não sabe qual função está sendo chamada. Como exemplo, aqui está um código Haskell (já que o equivalente em C seria confuso!):
Aqui a
bar
função está chamando seu argumentof
, que pode ser qualquer coisa. Portanto, o compilador não pode simplesmente compilarbar
com uma instrução de salto rápido, porque não sabe para onde ir. Em vez disso, o código para o qual geramosbar
fará a desreferênciaf
para descobrir para qual função está apontando e depois pulará para ela. É isso que despacho dinâmico significa.Ambos os exemplos são para funções . Você mencionou métodos , que podem ser considerados como um estilo particular de função despachada dinamicamente. Por exemplo, aqui estão alguns Python:
A
y.foo()
chamada usa despacho dinâmico, pois está pesquisando o valor dafoo
propriedade noy
objeto e chamando o que encontrar; ele não sabe quey
terá classeA
ou que aA
classe contém umfoo
método; portanto, não podemos simplesmente pular direto para ele.OK, essa é a ideia básica. Observe que o envio estático é mais rápido que o envio dinâmico, independentemente de compilarmos ou interpretarmos; tudo o resto é igual. A desreferenciação incorre em um custo extra de qualquer maneira.
Então, como isso afeta os compiladores modernos e otimizadores?
A primeira coisa a observar é que o envio estático pode ser otimizado com mais intensidade: quando sabemos para qual função estamos pulando, podemos fazer coisas como embutir. Com o envio dinâmico, não sabemos se estamos pulando até o tempo de execução, portanto não há muita otimização que possamos fazer.
Em segundo lugar, é possível em algumas línguas inferir para onde alguns despachos dinâmicos terminarão pulando e, portanto, otimizá-los em despacho estático. Isso nos permite realizar outras otimizações, como inlining etc.
No exemplo acima do Python, essa inferência é bastante inútil, já que o Python permite que outro código substitua classes e propriedades, por isso é difícil inferir muito do que é válido em todos os casos.
Se nosso idioma nos permitir impor mais restrições, por exemplo, limitando
y
a classeA
usando uma anotação, poderíamos usar essas informações para inferir a função de destino. Em linguagens com subclassificação (que é quase todas as linguagens com classes!), Isso na verdade não é suficiente, uma vez quey
pode realmente ter uma (sub) classe diferente; portanto, precisamos de informações adicionais, como asfinal
anotações de Java, para saber exatamente qual função será chamada.Haskell não é uma linguagem OO, mas podemos inferir o valor de
f
por inliningbar
(que é estaticamente despachado) emmain
, substituindofoo
paray
. Como o destino defoo
inmain
é estaticamente conhecido, a chamada se torna estaticamente despachada e provavelmente será incorporada e otimizada completamente (como essas funções são pequenas, é mais provável que o compilador as incline; embora não possamos contar com isso em geral )Portanto, o custo se resume a:
Se você estiver usando uma linguagem "muito dinâmica", com muito envio dinâmico e poucas garantias disponíveis para o compilador, todas as chamadas terão um custo. Se você estiver usando uma linguagem "muito estática", um compilador maduro produzirá código muito rápido. Se você estiver no meio, isso pode depender do seu estilo de codificação e do quão inteligente é a implementação.
fonte
Infelizmente, isso é altamente dependente de:
Primeiro de tudo, a primeira lei da otimização de desempenho é o primeiro perfil . Existem muitos domínios nos quais o desempenho da parte do software é irrelevante para o desempenho de toda a pilha: chamadas de banco de dados, operações de rede, operações de SO, ...
Isso significa que o desempenho do software é completamente irrelevante, mesmo que não melhore a latência. A otimização do software pode resultar em economia de energia e hardware (ou economia de bateria para aplicativos móveis), o que pode ser importante.
No entanto, esses NÃO costumam ter problemas oculares, e muitas vezes as melhorias algorítmicas superam as micro otimizações por uma grande margem.
Portanto, antes de otimizar, você precisa entender para o que está otimizando ... e se vale a pena.
Agora, com relação ao desempenho puro do software, ele varia muito entre as cadeias de ferramentas.
Existem dois custos para uma chamada de função:
O custo do tempo de execução é bastante óbvio; para executar uma chamada de função, é necessária uma certa quantidade de trabalho. Usando C no x86, por exemplo, uma chamada de função exigirá (1) espalhar registros na pilha, (2) enviar argumentos para os registradores, executar a chamada e depois (3) restaurar os registros da pilha. Veja este resumo das convenções de chamada para ver o trabalho envolvido .
Esse derramamento / restauração de registro leva uma quantidade não trivial de vezes (dezenas de ciclos da CPU).
Geralmente, espera-se que esse custo seja trivial em comparação com o custo real da execução da função, no entanto, alguns padrões são contraproducentes aqui: getters, funções protegidas por uma condição simples, etc.
Além dos intérpretes , um programador espera, portanto, que seu compilador ou JIT otimize as chamadas de função desnecessárias; embora essa esperança às vezes não dê frutos. Porque otimizadores não são mágicos.
Um otimizador pode detectar que uma chamada de função é trivial e incorporar a chamada: essencialmente, copie / cole o corpo da função no site da chamada. Isso nem sempre é uma boa otimização (pode induzir inchaço), mas geralmente vale a pena, porque o inlining expõe o contexto , e o contexto permite mais otimizações.
Um exemplo típico é:
Se
func
estiver embutido, o otimizador perceberá que a ramificação nunca é tomada e otimizarácall
paravoid call() {}
.Nesse sentido, as chamadas de função, ocultando informações do otimizador (se ainda não estiverem incorporadas), podem inibir certas otimizações. As chamadas de função virtual são especialmente culpadas disso, porque a destirtualização (provar que função é chamada em última instância no tempo de execução) nem sempre é fácil.
Em conclusão, meu conselho é escrever com clareza primeiro, evitando a pessimização algorítmica prematura (complexidade cúbica ou piores mordidas rapidamente) e depois otimizar apenas o que precisa ser otimizado.
fonte
Eu só vou dizer nunca. Acredito que a citação seja imprudente para apenas jogar lá fora.
É claro que não estou falando a verdade completa, mas não me importo em ser sincero tanto assim. É como naquele filme de Matrix, eu esqueci se era 1 ou 2 ou 3 - acho que foi aquele com a atriz italiana sexy com os grandes melões (eu realmente não gostei de nenhum, exceto o primeiro), quando oracle lady disse a Keanu Reeves: "Acabei de lhe dizer o que você precisava ouvir", ou algo nesse sentido, é isso que quero fazer agora.
Programadores não precisam ouvir isso. Se eles tiverem experiência com os criadores de perfil em suas mãos e a cotação for um pouco aplicável aos seus compiladores, eles já saberão disso e aprenderão isso da maneira correta, desde que compreendam sua saída de criação de perfil e por que determinadas chamadas em folha são pontos de acesso, através da medição. Se eles não são experientes e nunca criaram um perfil de seu código, esta é a última coisa que eles precisam ouvir, que devem começar a comprometer supersticiosamente a forma como escrevem o código até o ponto de incluir tudo antes mesmo de identificar pontos críticos na esperança de que isso aconteça. tornar-se mais eficiente.
Enfim, para uma resposta mais precisa, depende. Algumas das condições já estão listadas entre as boas respostas. As condições possíveis para a escolha de um idioma já são enormes, como o C ++, que teria que ser despachado dinamicamente em chamadas virtuais e quando ele pode ser otimizado e sob quais compiladores e até vinculadores, e que já justifica uma resposta detalhada e muito menos tentar para enfrentar as condições em todos os idiomas e compiladores possíveis. Mas vou acrescentar, "quem se importa?" porque, mesmo trabalhando em áreas críticas de desempenho como raytracing, a última coisa que começarei a fazer é abordar os métodos antes de fazer qualquer medição.
Eu acredito que algumas pessoas ficam muito zelosas ao sugerir que você nunca deve fazer micro-otimizações antes da medição. Se a otimização para a localidade das referências conta como uma micro-otimização, geralmente começo a aplicar essas otimizações logo no início com uma mentalidade de design orientada a dados em áreas que eu sei com certeza serão críticas ao desempenho (código de rastreamento de raios, por exemplo), porque, caso contrário, sei que terei que reescrever grandes seções logo depois de trabalhar nesses domínios por anos. A otimização da representação de dados para acertos no cache geralmente pode ter o mesmo tipo de aprimoramento de desempenho que os aprimoramentos algorítmicos, a menos que falemos do tempo quadrático ao linear.
Mas nunca vi um bom motivo para começar a incluir antes das medições, especialmente porque os criadores de perfil são decentes em revelar o que pode se beneficiar, mas não em revelar o que pode ser beneficiado por não estarem em linha (e não em linha pode realmente tornar o código mais rápido se o chamada de função sem linha é um caso raro, melhorando a localidade de referência para o icache para código quente e às vezes até permitindo que os otimizadores executem um trabalho melhor no caminho de execução comum do caso).
fonte