Comme d’autres le disent, vous devriez d’abord mesurer la performance de votre programme et vous ne constaterez probablement aucune différence dans la pratique.
Néanmoins, d'un point de vue conceptuel, je pensais éclaircir quelques points qui sont confondus dans votre question. Tout d'abord, vous demandez:
Les coûts des appels de fonctions comptent-ils encore dans les compilateurs modernes?
Remarquez les mots clés "fonction" et "compilateurs". Votre citation est subtile différente:
N'oubliez pas que le coût d'un appel de méthode peut être important, en fonction de la langue.
Il s’agit de méthodes , au sens orienté objet.
Bien que "fonction" et "méthode" soient souvent utilisés de manière interchangeable, il existe des différences quant au coût (dont vous parlez) et à la compilation (qui est le contexte que vous avez donné).
Nous devons en particulier connaître la répartition statique par rapport à la répartition dynamique . Je vais ignorer les optimisations pour le moment.
Dans un langage comme C, nous appelons généralement des fonctions à dispatch statique . Par exemple:
int foo(int x) {
return x + 1;
}
int bar(int y) {
return foo(y);
}
int main() {
return bar(42);
}
Lorsque le compilateur voit l'appel foo(y)
, il sait à quelle fonction son foo
nom fait référence, ainsi le programme de sortie peut passer directement à la foo
fonction, ce qui est relativement peu coûteux. C'est ce que l'envoi statique signifie.
L'alternative est la répartition dynamique , où le compilateur ne sait pas quelle fonction est appelée. Voici un exemple de code Haskell (car l’équivalent C serait désordonné!):
foo x = x + 1
bar f x = f x
main = print (bar foo 42)
Ici, la bar
fonction appelle son argument f
, ce qui pourrait être n'importe quoi. Par conséquent, le compilateur ne peut pas simplement compiler bar
une instruction de saut rapide, car il ne sait pas où aller. Au lieu de cela, le code que nous générons pour bar
déréférencera f
pour rechercher la fonction à laquelle il pointe, puis y accéder. C'est ce que l'envoi dynamique signifie.
Ces deux exemples concernent des fonctions . Vous avez mentionné les méthodes , qui peuvent être considérées comme un style particulier de fonction à distribution dynamique. Par exemple, voici quelques exemples de 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)
L' y.foo()
appel utilise la répartition dynamique, puisqu'il recherche la valeur de la foo
propriété dans l' y
objet et appelle tout ce qu'il trouve. il ne sait pas qu'il y
aura une classe A
, ou que la A
classe contient une foo
méthode, nous ne pouvons donc pas y aller directement.
OK, c'est l'idée de base. Notez que l' envoi statique est plus rapide que l' envoi dynamique indépendamment du fait que nous compilons ou interprétons; tout le reste étant égal. Le déréférencement entraîne un coût supplémentaire dans les deux sens.
Alors, comment cela affecte-t-il les compilateurs modernes et optimisants?
La première chose à noter est que la répartition statique peut être optimisée plus lourdement: lorsque nous savons à quelle fonction nous accédons, nous pouvons faire des choses comme l’intégration en ligne. Avec la répartition dynamique, nous ne savons pas que nous sautons jusqu'au moment de l'exécution, de sorte que nous ne pouvons pas optimiser beaucoup.
Deuxièmement, dans certaines langues, il est possible de déduire où certaines dépêches dynamiques finiront par sauter, et donc de les optimiser en répartition statique. Cela nous permet d’effectuer d’autres optimisations comme l’alignement, etc.
Dans l'exemple Python ci-dessus, une telle inférence est plutôt sans espoir, car Python permet à un autre code de remplacer les classes et les propriétés. Il est donc difficile d'en déduire ce qui va se passer dans tous les cas.
Si notre langage nous permet d'imposer davantage de restrictions, par exemple en limitant la y
classe à l' A
aide d'une annotation, nous pourrions utiliser cette information pour déduire la fonction cible. Dans les langues avec sous-classes (c'est-à-dire presque toutes les langues avec classes!), Cela ne suffit pas, car il y
peut en fait avoir une (sous) classe différente. Nous aurions donc besoin d'informations supplémentaires comme les final
annotations de Java pour savoir exactement quelle fonction sera appelée.
Haskell n'est pas un langage OO, mais nous pouvons déduire la valeur de f
inlining bar
(qui est statiquement expédié) dans main
, en remplaçant foo
par y
. Puisque la cible de foo
in main
est connue de manière statique, l'appel est envoyé de manière statique et sera probablement totalement aligné et optimisé (ces fonctions étant petites, le compilateur est plus susceptible de les aligner; nous ne pouvons cependant pas compter sur cela en général. ).
Le coût revient donc à:
- La langue envoie-t-elle votre appel de manière statique ou dynamique?
- Si c'est le dernier cas, le langage permet-il à l'implémentation d'inférer la cible en utilisant d'autres informations (par exemple, types, classes, annotations, inline, etc.)?
- Dans quelle mesure la répartition statique (déduite ou non) peut-elle être optimisée?
Si vous utilisez un langage "très dynamique", avec une bonne répartition dynamique et peu de garanties pour le compilateur, chaque appel engendrera un coût. Si vous utilisez un langage "très statique", un compilateur mature produira un code très rapide. Si vous êtes entre les deux, cela peut dépendre de votre style de codage et du degré d'implémentation de votre mise en œuvre.