baisse importante des performances avec gcc, peut-être liée à inline

Je rencontre actuellement un effet étrange avec gcc (version testée: 4.8.4).

J’ai un code orienté performance, qui tourne assez vite. Sa vitesse dépend en grande partie de l’inclusion de nombreuses petites fonctions.

Comme il est difficile d’introduire plusieurs fichiers .c dans le répertoire ( -flto n’est pas encore largement disponible), j’ai conservé de nombreuses petites fonctions (généralement de 1 à 5 lignes de code chacune) dans un fichier C commun dans lequel je développe. un codec et son décodeur associé. C’est «relativement» important par rapport à ma norme (environ 2 000 lignes, même si la plupart ne sont que des commentaires et des lignes vierges), mais le diviser en parties plus petites pose de nouveaux problèmes. Je préférerais donc éviter cela, si cela est possible.

Le codeur et le décodeur sont liés, car ce sont des opérations inverses. Mais du sharepoint vue de la programmation, ils sont complètement séparés et ne partagent aucun point commun, à l’exception de quelques fonctions typées et de fonctions de bas niveau (telles que la lecture depuis une position mémoire non alignée).

L’effet étrange est celui-ci:

J’ai récemment ajouté une nouvelle fonction à l’encodeur. C’est un nouveau “point d’entrée”. Il n’est ni utilisé ni appelé de n’importe où dans le fichier .c .

Le simple fait qu’elle existe fdec considérablement les performances de la fonction de décodage fdec , de plus de 20%, ce qui est bien trop pour être ignoré.

Maintenant, gardez à l’esprit que les opérations d’encodage et de décodage sont complètement séparées et ne partagent presque rien, sauf quelques modifications mineures de typedef ( u32 , u16 et autres) et les opérations associées (lecture / écriture).

Lorsque vous définissez la nouvelle fonction de fnew comme static , les performances du décodeur fdec augmentent à nouveau. Puisque fnew n’est pas appelé depuis le fnew , j’imagine que c’est la même chose que s’il n’y avait pas lieu (élimination du code mort).

Si static fnew est maintenant appelé du côté encodeur, les performances de fdec restnt fdec .

Mais dès que fnew est modifié, les performances fdec chutent considérablement.

En fnew modifications fnew un seuil, j’ai augmenté le paramètre gcc suivant: --param max-inline-insns-auto=60 (par défaut, sa valeur est supposée être 40.) Et cela a fonctionné: les performances de fdec sont maintenant de retour à Ordinaire.

Et je suppose que ce jeu continuera indéfiniment avec chaque petite modification du fnew ou de quelque chose d’autre semblable, nécessitant un ajustement supplémentaire.

C’est tout simplement bizarre. Il n’y a aucune raison logique pour qu’une petite modification de la fonction fnew ait un effet d’ fdec fonction complètement indépendante fdec , laquelle seule relation doit figurer dans le même fichier.

La seule explication provisoire que je puisse inventer à ce jour est que peut-être la simple présence de fnew suffit-elle à franchir une sorte de global file threshold qui aurait un impact sur fdec . fnew peut être rendu “non présent” quand il est: 1. pas là, 2. static mais pas appelé de partout 3. static et suffisamment petit pour être en ligne. Mais ce n’est que cacher le problème. Est-ce que cela signifie que je ne peux pas append de nouvelle fonction?

Vraiment, je n’ai trouvé aucune explication satisfaisante sur le net.

J’étais curieux de savoir si quelqu’un avait déjà eu un effet secondaire équivalent et y avait trouvé une solution.

[Modifier]

Allons-y pour un test plus fou. Maintenant, j’ajoute une autre fonction complètement inutile, juste pour jouer avec. Son contenu est ssortingctement un copier-coller de fnew , mais le nom de la fonction est évidemment différent, appelons-le wtf .

Lorsque wtf existe, peu importe que fnew soit statique ou non, ni quelle est la valeur de max-inline-insns-auto : les performances de fdec sont fdec normales. Même si wtf n’est ni utilisé ni appelé de nulle part …: ‘(

[Edit 2] il n’y a pas d’instruction en inline . Toutes les fonctions sont normales ou static . La décision en ligne est uniquement du ressort du compilateur, ce qui a bien fonctionné jusqu’à présent.

[Edit 3] Comme Peter Cordes l’a suggéré, le problème n’est pas lié à l’alignement, mais à l’alignement des instructions. Sur les processeurs Intel plus récents (Sandy Bridge et versions ultérieures), les boucles chaudes bénéficient d’un alignement sur des limites de 32 octets. Le problème est que, par défaut, gcc aligne sur des limites de 16 octets. Ce qui donne 50% de chances d’être correctement aligné en fonction de la longueur du code précédent. D’où un problème difficile à comprendre, qui “semble aléatoire”.

Toutes les boucles ne sont pas sensibles. Cela ne concerne que les boucles critiques, et seulement si leur longueur les fait traverser un segment d’instruction de 32 octets supplémentaire lorsqu’elles sont moins parfaitement alignées.

    Faire de mes commentaires une réponse, parce que cela devenait une longue discussion. La discussion a montré que le problème de performance est sensible à l’alignement.

    Vous trouverez des liens vers des informations sur les réglages parfaits à l’ adresse https://stackoverflow.com/tags/x86/info , notamment le guide d’optimisation d’Intel et les excellentes informations d’Agner Fog. Certains conseils d’optimisation de l’assemblage d’Agner Fog ne s’appliquent pas pleinement aux processeurs Sandybridge et ultérieurs. Toutefois, si vous souhaitez des informations détaillées sur un processeur spécifique, le guide microarch est très bon.

    Sans au moins un lien externe au code que je peux essayer moi-même, je ne peux pas faire plus que de la main. Si vous n’enregistrez pas le code, vous devrez utiliser des outils de comptage des performances de processeur / processeur, tels que les performances Linux ou Intel VTune, afin de détecter ce problème dans un délai raisonnable.


    Dans le chat, le PO a trouvé quelqu’un d’autre qui avait ce problème, mais avec du code posté . C’est probablement le même problème que le PO rencontre et c’est l’un des principaux moyens d’aligner les codes d’alignement pour les caches uop de style Sandybridge.

    Il y a une limite de 32B au milieu de la boucle dans la version lente. Les instructions qui commencent avant la limite se décodent en 5 uops. Ainsi, dans le premier cycle, le cache uop sert à mov/add/movzbl/mov . Au deuxième cycle, il ne rest plus qu’un seul mouvement dans la ligne de cache actuelle. Ensuite, le 3ème cycle émet les 2 derniers uops de la boucle: add et cmp+ja .

    Le mov problématique commence à 0x..ff . Je suppose que les instructions qui couvrent une limite de 32B vont dans (une des) cacheline (s) uop pour leur adresse de départ.

    Dans la version rapide, une itération ne prend que 2 cycles à émettre: le même premier cycle, puis mov / add / cmp+ja au second.

    Si l’une des 4 premières instructions avait été un octet plus long (par exemple, complétée avec un préfixe inutile ou un préfixe REX), il n’y aurait pas de problème. Il n’y aurait pas d’exception à la fin de la première cacheline, car la commande mov commencerait après la limite de 32B et ferait partie de la prochaine ligne de cache uop.

    Pour autant que je sache, la sortie du déassembly est le seul moyen d’utiliser des versions plus longues des mêmes instructions (voir Optimisation de l’assemblage d’Agner Fog) pour obtenir des limites de 32B à des multiples de 4 uops. Je ne connais pas d’interface graphique qui montre l’alignement du code assemblé à mesure que vous éditez. (Et évidemment, cela ne fonctionne que pour les écritures manuscrites, et est fragile. Changer le code va casser l’alignement manuel.)

    C’est pourquoi le guide d’optimisation d’Intel recommande d’aligner les boucles critiques sur 32B.

    Ce serait vraiment bien si un assembleur avait le moyen de demander que les instructions précédentes soient assemblées en utilisant des codages plus longs pour respecter une certaine longueur. Peut-être une paire de directives .startencodealign / .endencodealign 32 , pour appliquer un remplissage au code entre les directives afin de la terminer sur une limite de 32B. Cela pourrait faire un code terrible si mal utilisé, cependant.


    Les modifications apscopes au paramètre inlining modifieront la taille des fonctions et remplaceront le code par un multiple de 16B. Cet effet est similaire à la modification du contenu d’une fonction: il s’agrandit et modifie l’alignement des autres fonctions.

    Je m’attendais à ce que le compilateur s’assure toujours qu’une fonction commence à la position idéale alignée, en utilisant noop pour combler les espaces vides.

    Il y a un compromis. Aligner chaque fonction sur 64B (le début d’une ligne de cache) nuirait aux performances. La densité du code diminuerait, avec davantage de lignes de cache nécessaires pour contenir les instructions. 16B est bon, car il s’agit de l’instruction d’extraction / décodage de la taille de bloc sur la plupart des processeurs récents.

    Agner Fog a les détails de bas niveau pour chaque microarch. Il ne l’a pas mise à jour pour Broadwell, cependant, mais le cache uop n’a probablement pas changé depuis Sandybridge. Je suppose qu’il y a une assez petite boucle qui domine le temps d’exécution. Je ne sais pas exactement quoi chercher en premier. Peut-être que la version “lente” a des cibles de twig proches de la fin d’un bloc de code 32B (et donc de la fin d’une cacheline d’uop), ce qui entraîne nettement moins de 4 uops par horloge sortant de l’interface.

    Examinez les compteurs de performance pour les versions “lente” et “rapide” (par exemple, avec perf stat ./cmd ) et voyez s’ils sont différents. Par exemple, beaucoup plus de cache manquants pourraient indiquer un faux partage d’une ligne de cache entre les threads. En outre, profil et voir s’il y a un nouveau point chaud dans la version “lente”. (par exemple avec perf record ./cmd && perf report sous Linux).

    Combien d’ops / horloge la version “rapide” est-elle en train de recevoir? Si elle est supérieure à 3, les goulots d’étranglement frontaux (peut-être dans le cache uop) qui sont sensibles à l’alignement peuvent être le problème. Soit cela, soit L1 / uop-cache manque si un alignement différent signifie que votre code a besoin de plus de lignes de cache que celles disponibles.

    Quoi qu’il en soit, il convient de répéter ceci: utilisez un compteur de performances / compteurs pour rechercher le nouveau goulot d’étranglement que la version “lente” a, mais pas la version “rapide”. Ensuite, vous pouvez passer du temps à regarder le déassembly de ce bloc de code. (Ne regardez pas la sortie asm de gcc. Vous devez voir l’alignement dans le désassemblage du binary final.) Regardez les limites 16B et 32B, car elles se trouveront probablement à des endroits différents entre les deux versions, et nous pensons c’est la cause du problème.

    L’alignement peut également faire échouer la macro-fusion si une comparaison / jcc divise une limite de 16B exactement. Bien que cela soit peu probable dans votre cas, vos fonctions sont toujours alignées sur un multiple de 16B.

    Outils automatisés d’alignement: non, je ne suis au courant de rien qui puisse regarder un binary et vous dire quoi que ce soit d’utile sur l’alignement. J’aimerais qu’il y ait un éditeur pour montrer des groupes de 4 uops et 32B aux côtés de votre code et les mettre à jour au fur et à mesure de vos modifications.

    L’IACA d’Intel peut parfois être utile pour parsingr une boucle, mais l’IIRC ne connaît pas les twigs sockets, et je pense qu’il ne dispose pas d’un modèle sophistiqué de l’interface, ce qui est évidemment le problème si un mauvais alignement réduit les performances pour vous.

    D’après mon expérience, les baisses de performances peuvent être provoquées par la désactivation de l’optimisation en ligne.

    Le modificateur “inline” n’indique pas de forcer une fonction à être en ligne. Cela donne aux compilateurs un indice pour intégrer une fonction. Ainsi, lorsque les critères d’optimisation en ligne du compilateur ne seront pas satisfaits par des modifications de code sortingviales, une fonction modifiée avec inline est normalement compilée en une fonction statique.

    Et il y a une chose qui rend le problème plus complexe, des optimisations en ligne nestedes. Si vous avez une fonction en ligne, fA, qui appelle une fonction en ligne, fB, comme ceci:

     inline void fB(int x, int y) { return x * y; } inline void fA() { for(int i = 0; i < 0x10000000; ++i) { fB(i, i+1); } } void main() { fA(); } 

    Dans ce cas, nous nous attendons à ce que fA et fB soient en ligne. Mais si les critères en ligne ne sont pas remplis, les performances ne peuvent être prévisibles. C'est-à-dire que d'importantes baisses de performances se produisent lorsque l'inline est désactivé sur fB, mais de très légères baisses pour fA. Et vous savez, les décisions internes du compilateur sont beaucoup plus complexes.

    Les raisons entraînent la désactivation de l'inline, par exemple la taille de la fonction, la taille du fichier .c, le nombre de variables locales, etc.

    En fait, en C #, cette performance a chuté. Dans mon cas, une baisse de performance de 60% se produit lorsqu'une variable locale est ajoutée à une fonction simple en ligne.

    MODIFIER:

    Vous pouvez rechercher ce qui se passe en lisant le code d'assembly compilé. Je suppose qu'il existe de véritables appels inattendus à des fonctions modifiées avec «inline».