Como os outros dizem, você deve medir primeiro o desempenho do seu programa, e provavelmente não encontrará diferença na prática.
Ainda assim, a partir de um nível conceitual, achei que esclareceria algumas coisas que estão confundidas em sua pergunta. Em primeiro lugar, você pergunta:
Do function call costs still matter in modern compilers?
Observe as palavras-chave "função" e "compiladores". Sua cotação é sutilmente diferente:
Remember that the cost of a method call can be significant, depending on the language.
Isto está falando sobre métodos , no sentido orientado a objeto.
Embora "função" e "método" sejam frequentemente usados de forma intercambiável, há diferenças quando se trata de seu custo (sobre o qual você está perguntando) e quando se trata de compilação (que é o contexto que você deu).
Em particular, precisamos saber sobre despacho estático vs despacho dinâmico . Eu vou ignorar otimizações para o 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 o nome foo
está se referindo, então o programa de saída pode ir direto para a função foo
, que é bem barata. Isso é o que despacho estático significa.
A alternativa é despacho dinâmico , onde o compilador não sabe qual função está sendo chamada. Por exemplo, aqui está um código Haskell (já que o equivalente C seria confuso!):
foo x = x + 1
bar f x = f x
main = print (bar foo 42)
Aqui, a função bar
está chamando seu argumento f
, que pode ser qualquer coisa. Portanto, o compilador não pode simplesmente compilar bar
para uma instrução de salto rápido, porque não sabe para onde ir. Em vez disso, o código que geramos para bar
cancelará a referência f
para descobrir para qual função ele está apontando e, em seguida, passará para ele. Isso é o que envio 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á um pouco do 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 chamada y.foo()
usa o despacho dinâmico, pois está procurando o valor da propriedade foo
no objeto y
e chamando o que encontrar; ele não sabe que y
terá a classe A
ou que a classe A
contém um método foo
, por isso não podemos simplesmente ir direto para ela.
OK, essa é a ideia básica. Note que o despacho estático é mais rápido que o despacho dinâmico independentemente de compilar ou interpretar; tudo o mais sendo igual. A desreferenciação incorre em um custo extra de qualquer forma.
Então, como isso afeta os compiladores modernos e otimizadores?
A primeira coisa a notar é que o despacho estático pode ser otimizado mais strongmente: quando sabemos para qual função estamos indo, podemos fazer coisas como inlining. Com o despacho dinâmico, não sabemos se estamos pulando até o tempo de execução, por isso não há muita otimização que possamos fazer.
Em segundo lugar, é possível em algumas línguas inferir onde alguns despachos dinâmicos terminarão, e assim otimizá-los para o despacho estático. Isso nos permite realizar outras otimizações, como inlining, etc.
No exemplo acima do Python, tal inferência é bastante inútil, uma vez que o Python permite que outro código substitua classes e propriedades, portanto é difícil deduzir muito do que será válido em todos os casos.
Se a nossa linguagem nos permitir impor mais restrições, por exemplo, limitando y
à classe A
usando uma anotação, poderíamos usar essas informações para inferir a função de destino. Em linguagens com subclasses (que são quase todas as linguagens com classes!) Isso não é suficiente, pois y
pode ter uma (sub) classe diferente, então precisaríamos de informações extras como as anotações final
do Java para saber exatamente quais função será chamada.
Haskell não é uma linguagem OO, mas podemos inferir o valor de f
inlining bar
(que é estaticamente despachado) em main
, substituindo foo
por% código%. Como o destino de y
in foo
é estatisticamente conhecido, a chamada se torna estaticamente despachada e provavelmente será alinhada e otimizada completamente (já que essas funções são pequenas, é mais provável que o compilador as incorpore; embora possamos ' conte com isso em geral).
Por isso, o custo se resume a:
- O idioma envia sua chamada estaticamente ou dinamicamente?
- Se for o último, a linguagem permite que a implementação deduza o alvo usando outras informações (por exemplo, tipos, classes, anotações, inlining, etc.)?
- Quão agressivamente o despacho estático (inferido ou não) pode ser otimizado?
Se você estiver usando uma linguagem "muito dinâmica", com muito despacho dinâmico e poucas garantias disponíveis para o compilador, todas as chamadas incorrerão em um custo. Se você estiver usando uma linguagem "muito estática", um compilador maduro produzirá um código muito rápido. Se você está no meio, então pode depender do seu estilo de codificação e de quão inteligente é a implementação.