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:
Os custos das chamadas de função ainda são importantes nos compiladores modernos?
Observe as palavras-chave "função" e "compiladores". Sua cotação é sutilmente diferente:
Lembre-se de que o custo de uma chamada de método pode ser significativo, dependendo do idioma.
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:
int foo(int x) {
return x + 1;
}
int bar(int y) {
return foo(y);
}
int main() {
return bar(42);
}
Quando o compilador vê a chamada foo(y)
, ele sabe a qual função esse foo
nome está se referindo, para que o programa de saída possa ir direto para a foo
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!):
foo x = x + 1
bar f x = f x
main = print (bar foo 42)
Aqui a bar
função está chamando seu argumento f
, que pode ser qualquer coisa. Portanto, o compilador não pode simplesmente compilar bar
com uma instrução de salto rápido, porque não sabe para onde ir. Em vez disso, o código para o qual geramos bar
fará a desreferência f
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:
class A:
def __init__(self, x):
self.x = x
def foo(self):
return self.x + 1
def bar(y):
return y.foo()
z = A(42)
bar(z)
A y.foo()
chamada usa despacho dinâmico, pois está pesquisando o valor da foo
propriedade no y
objeto e chamando o que encontrar; ele não sabe que y
terá classe A
ou que a A
classe contém um foo
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 classe A
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 que y
pode realmente ter uma (sub) classe diferente; portanto, precisamos de informações adicionais, como as final
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 inlining bar
(que é estaticamente despachado) em main
, substituindo foo
para y
. Como o destino de foo
in main
é 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:
- O idioma despacha sua chamada estática ou dinamicamente?
- Se for o último, a linguagem permite que a implementação infere o destino usando outras informações (por exemplo, tipos, classes, anotações, inlining etc.)?
- Com que agressividade o despacho estático (inferido ou não) pode ser otimizado?
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.