Pourquoi les compilateurs n'alignent-ils pas tout? [fermé]

13

Parfois, les compilateurs appellent des fonctions en ligne. Cela signifie qu'ils déplacent le code de la fonction appelée dans la fonction appelante. Cela rend les choses un peu plus rapides car il n'est pas nécessaire de pousser et de faire apparaître des éléments dans et hors de la pile d'appels.

Donc ma question est, pourquoi les compilateurs n'incluent-ils pas tout? Je suppose que cela rendrait l'exécutable beaucoup plus rapide.

La seule raison pour laquelle je peux penser est un exécutable beaucoup plus grand, mais est-ce vraiment important de nos jours avec des centaines de Go de mémoire? L'amélioration des performances n'en vaut-elle pas la peine?

Y a-t-il une autre raison pour laquelle les compilateurs n'incluent pas seulement tous les appels de fonction?

Aviv Cohn
la source
18
IDK à propos de vous, mais je n'ai pas des centaines de Go de mémoire.
Ampt
2
Isn't the improved performance worth it?Pour une méthode qui exécutera une boucle 100 fois et calculera quelques chiffres sérieux, la surcharge de déplacement de 2 ou 3 arguments vers les registres CPU n'est rien.
Doval
5
Vous êtes trop générique, est-ce que «compilateurs» signifie «tous les compilateurs» et «tout» signifie vraiment «tout»? Ensuite, la réponse est simple, il y a des situations où vous ne pouvez tout simplement pas vous aligner. La récursivité me vient à l'esprit.
Otávio Décio
17
La localisation du cache est beaucoup plus importante que la minuscule surcharge des appels de fonction.
SK-logic
3
L'amélioration des performances est-elle vraiment importante de nos jours avec des centaines de GFLOPS de puissance de traitement?
mouviciel

Réponses:

22

Notez tout d'abord que l'un des effets majeurs de l'inline est qu'il permet de réaliser de nouvelles optimisations sur le site de l'appel.

Pour votre question: il y a des choses difficiles, voire impossibles à intégrer:

  • bibliothèques liées dynamiquement

  • fonctions déterminées dynamiquement (répartition dynamique, appelées via des pointeurs de fonction)

  • fonctions récursives (la récursion de queue peut)

  • fonctions pour lesquelles vous n'avez pas le code (mais l'optimisation du temps de liaison le permet pour certaines d'entre elles)

L'intégration n'a pas seulement des effets bénéfiques:

  • un exécutable plus grand signifie plus d'espace disque et plus de temps de chargement

  • un exécutable plus grand signifie une augmentation de la pression du cache (notez que l'inclusion de fonctions suffisamment petites telles que de simples getters peut réduire la taille de l'exécutable et la pression du cache)

Et enfin, pour les fonctions qui prennent un temps non trivial à exécuter, le gain ne vaut tout simplement pas la peine.

AProgrammer
la source
3
certains appels récursifs peuvent être intégrés (appels de queue), mais tous peuvent être transformés en itération si vous ajoutez éventuellement une pile explicite
ratchet freak
@ratchetfreak, vous pouvez également transformer un appel récursif non-queue en queue un. Mais c'est pour moi dans le domaine du "difficile" (surtout lorsque vous avez des fonctions co-récursives ou devez déterminer dynamiquement où sauter pour simuler le retour), mais ce n'est pas impossible (vous venez de mettre en place un cadre de continuation et considérant que le présent devient plus facile).
AProgrammer
11

Le polymorphisme d'exécution est une limitation majeure. S'il y a une répartition dynamique qui se produit lorsque vous écrivez, foo.bar()il est impossible d'inline l'appel de méthode. Cela explique pourquoi les compilateurs n'incluent pas tout.

Les appels récursifs ne peuvent pas non plus être facilement intégrés.

L'intégration de modules croisés est également difficile à réaliser pour des raisons techniques (une recompilation incrémentielle serait impossible par ex)

Cependant, les compilateurs font beaucoup de choses en ligne.

Simon Bergot
la source
3
Il est très difficile de s'aligner via une répartition virtuelle, mais pas impossible. Certains compilateurs C ++ sont capables de le faire dans certaines circonstances.
bstamour
2
... ainsi que certains compilateurs JIT (dévirtualisation).
Frank
@bstamour Tout compilateur semi-décent de n'importe quel langage avec les optimisations appropriées sur enverra statiquement, c'est-à-dire dévirtualise, un appel à une méthode déclarée virtuelle sur un objet dont le type dynamique est connaissable au moment de la compilation. Cela peut faciliter l'incrustation si la phase de dévirtualisation a lieu avant la (ou une autre) phase d'incrustation. Mais c'est trivial. Y avait-il autre chose que vous vouliez dire? Je ne vois pas comment un "Inline via une dépêche virtuelle" peut être réalisé. Pour être en ligne, il faut connaître le type statique - c'est-à-dire dévirtualiser - donc l'existence de l'inline signifie qu'il n'y a pas de répartition virtuelle
underscore_d
9

Premièrement, vous ne pouvez pas toujours être en ligne, par exemple les fonctions récursives peuvent ne pas être toujours en ligne (mais un programme contenant une définition récursive factavec juste une impression de fact(8)pourrait être en ligne).

Ensuite, l'inline n'est pas toujours bénéfique. Si le compilateur s'aligne tellement que le code de résultat est assez grand pour que ses parties chaudes ne rentrent pas dans le cache d'instructions L1 par exemple, il pourrait être beaucoup plus lent que la version non intégrée (qui s'adapterait facilement au cache L1) ... De plus, les processeurs récents exécutent très rapidement une CALLinstruction machine (au moins vers un emplacement connu, c'est-à-dire un appel direct, pas un appel via un pointeur).

Enfin, une intégration complète nécessite une analyse complète du programme. Cela pourrait ne pas être possible (ou est trop coûteux). Avec C ou C ++ compilé par GCC (et aussi avec Clang / LLVM ), vous devez activer l' optimisation du temps de liaison (en compilant et en liant avec par exemple g++ -flto -O2) et cela prend beaucoup de temps de compilation.

Basile Starynkevitch
la source
1
Pour mémoire, LLVM / Clang (et plusieurs autres compilateurs) prend également en charge l'optimisation du temps de liaison .
Vous
Je le sais; LTO existait au siècle précédent (IIRC, dans certains compilateurs propriétaires MIPS au moins).
Basile Starynkevitch
7

Aussi surprenant que cela puisse paraître, le fait de tout inclure ne réduit pas nécessairement le temps d'exécution. La taille accrue de votre code peut rendre difficile pour le CPU de garder tout votre code dans son cache à la fois. Un échec de cache sur votre code devient plus probable et un échec de cache coûte cher. Cela est encore pire si vos fonctions potentiellement intégrées sont volumineuses.

J'ai eu des améliorations de performances notables de temps en temps en retirant de gros morceaux de code marqués comme `` en ligne '' des fichiers d'en-tête, en les plaçant dans le code source, de sorte que le code ne se trouve qu'à un seul endroit plutôt que sur chaque site d'appel. Ensuite, le cache du processeur est mieux utilisé et vous obtenez également un meilleur temps de compilation ...

Tom Tanner
la source
cela semble simplement répéter les points soulevés et expliqués dans une réponse précédente qui a été publiée il y a une heure
moucher
1
Quels caches? L1? L2? L3? Lequel est le plus important?
Peter Mortensen
1

Tout intégrer ne signifierait pas seulement une augmentation de la consommation de mémoire disque, mais également une augmentation de la consommation de mémoire interne, ce qui n'est pas si abondant. N'oubliez pas que le code repose également en mémoire dans le segment de code; si une fonction est appelée à partir de 10 000 emplacements (par exemple, ceux provenant de bibliothèques standard dans un projet assez important), le code de cette fonction occupe 10 000 fois plus de mémoire interne.

Une autre raison pourrait être les compilateurs JIT; si tout est en ligne, il n'y a pas de points chauds à compiler dynamiquement.

m3th0dman
la source
1

Premièrement, il existe des exemples simples où tout ce qui fonctionne fonctionnera très mal. Considérez ce code C simple:

void f1 (void) { printf ("Hello, world\n"); }
void f2 (void) { f1 (); f1 (); f1 (); f1 (); }
void f3 (void) { f2 (); f2 (); f2 (); f2 (); }
...
void f99 (void) { f98 (); f98 (); f98 (); f98 (); }

Devinez ce que tout cela vous fera.

Ensuite, vous faites l'hypothèse que l'inclusion accélérera les choses. C'est parfois le cas, mais pas toujours. L'une des raisons est que le code qui tient dans le cache d'instructions s'exécute beaucoup plus rapidement. Si j'appelle une fonction à partir de 10 emplacements, je vais toujours exécuter du code qui se trouve dans le cache d'instructions. S'il est en ligne, les copies sont partout et s'exécutent beaucoup plus lentement.

Il y a d'autres problèmes: l'inlining produit d'énormes fonctions. Les énormes fonctions sont beaucoup plus difficiles à optimiser. J'ai obtenu des gains considérables de code critique en termes de performances en masquant des fonctions dans un fichier séparé pour empêcher le compilateur de les intégrer. Par conséquent, le code généré pour ces fonctions était bien meilleur lorsqu'elles étaient masquées.

BTW. Je n'ai pas "des centaines de Go de mémoire". Mon ordinateur de travail n'a même pas "des centaines de Go d'espace disque dur". Et si mon application comptait "des centaines de Go de mémoire", cela prendrait 20 minutes juste pour charger l'application en mémoire.

gnasher729
la source