aliasing ssortingct et alignement de la mémoire

J’ai un code critique pour les performances et il existe une fonction énorme qui alloue comme 40 tableaux de taille différente sur la stack au début de la fonction. La plupart de ces tableaux doivent avoir un certain alignement (car ils sont accessibles ailleurs en utilisant des instructions CPU qui nécessitent un alignement mémoire (pour les processeurs Intel et arm).

Étant donné que certaines versions de gcc ne parviennent tout simplement pas à aligner correctement les variables de stack (notamment pour le code arm), ou même parfois, un alignement maximal de l’architecture cible est inférieur à ce que mon code demande en réalité, je n’ai tout simplement pas d’autre choix que d’allouer ces tableaux sur la stack et alignez-les manuellement.

Donc, pour chaque tableau, je dois faire quelque chose comme ça pour l’aligner correctement:

short history_[HIST_SIZE + 32]; short * history = (short*)((((uintptr_t)history_) + 31) & (~31)); 

De cette façon, l’ history est maintenant aligné sur une limite de 32 octets. Faire la même chose est fastidieux pour les 40 tableaux, plus cette partie du code est très intensive en processeur et je ne peux tout simplement pas utiliser la même technique d’alignement pour chacun des tableaux (ce désordre d’alignement confond l’optimiseur et une allocation de registre différente ralentit considérablement la fonction , pour une meilleure explication, voir explication à la fin de la question).

Alors … évidemment, je veux effectuer cet alignement manuel une seule fois et supposer que ces tableaux sont situés les uns à la suite des autres. J’ai également ajouté un remplissage supplémentaire à ces tableaux afin qu’ils soient toujours multiples de 32 octets. Alors, je crée simplement un tableau de caractères enorme sur la stack et le transforme en une structure qui a tous ces tableaux alignés:

 struct tmp { short history[HIST_SIZE]; short history2[2*HIST_SIZE]; ... int energy[320]; ... }; char buf[sizeof(tmp) + 32]; tmp * X = (tmp*)((((uintptr_t)buf) + 31) & (~31)); 

Quelque chose comme ca. Ce n’est peut-être pas le plus élégant, mais il a donné de très bons résultats et l’inspection manuelle de l’assemblage généré prouve que le code généré est plus ou moins adéquat et acceptable. Le système de compilation a été mis à jour pour utiliser le nouveau GCC et nous avons soudainement commencé à avoir des artefacts dans les données générées (par exemple, la sortie de la suite de tests de validation n’est plus exacte, même dans la compilation C pure avec du code asm désactivé). Le débogage du problème a pris beaucoup de temps et il a semblé être lié aux règles de création d’alias et aux nouvelles versions de GCC.

Alors, comment puis-je le faire? S’il vous plaît, ne perdez pas de temps à essayer d’expliquer que ce n’est pas standard, ni portable, indéfini, etc. (j’ai lu de nombreux articles à ce sujet). En outre, il n’ya aucun moyen de changer le code (j’envisagerais peut-être aussi de modifier GCC pour résoudre le problème, mais pas de refactoriser le code) … tout ce que je veux, c’est d’appliquer un sortilège de magie noire pour que les nouveaux GCC produit le même code de fonctionnement pour ce type de code sans désactiver les optimisations?

Modifier:

  • J’ai utilisé ce code sur plusieurs systèmes d’exploitation / compilateurs, mais j’ai commencé à avoir des problèmes lorsque je suis passé au nouveau NDK, basé sur GCC 4.6. J’obtiens le même mauvais résultat avec GCC 4.7 (de NDK r8d)
  • Je mentionne un alignement de 32 octets. Si cela vous fait mal aux yeux, remplacez-le par un autre chiffre, par exemple 666, si cela vous aide. Il est absolument inutile de mentionner que la plupart des architectures n’ont pas besoin de cet alignement. Si j’aligne 8 Ko de tableaux locaux sur la stack, je perds 15 octets pour un alignement de 16 octets et 31 pour un alignement de 32 octets. J’espère que ce que je veux dire est clair.
  • Je dis qu’il y a environ 40 baies sur la stack dans le code critique de performance. J’ai probablement aussi besoin de dire que c’est un ancien code de tierce partie qui fonctionne bien et que je ne veux pas le gâcher. Pas besoin de dire si c’est bon ou mauvais, pas de point pour ça.
  • Ce code / fonction a un comportement bien testé et défini. Nous avons le nombre exact d’exigences de ce code, par exemple, il alloue Xkb ou RAM, utilise Y kb de tables statiques et consum jusqu’à Z kb d’espace de stack et il ne peut pas changer, car le code ne sera pas modifié.
  • En disant que “le désordre de l’alignement confond l’optimiseur”, je veux dire que si j’essaie d’aligner chaque tableau séparément, l’optimiseur de code alloue des registres supplémentaires pour le code d’alignement et des parties de code critiques en termes de performances ne disposent pas soudainement d’un nombre suffisant de registres et entraîne un ralentissement du code. Ce comportement a été observé sur les processeurs ARM (au fait, Intel ne m’inquiète pas du tout).
  • Par artefacts, je veux dire que la sortie devient non-bitexact, il y a du bruit ajouté. En raison de ce problème d’aliasing de type ou d’un bogue dans le compilateur entraînant une sortie incorrecte de la fonction.
  • En bref, le sharepoint la question … comment puis-je allouer une quantité aléatoire d’espace de stack (en utilisant des tableaux de caractères ou alloca , puis aligner le pointeur sur cet espace de stack et réinterpréter ce bloc de mémoire comme une structure ayant une disposition bien définie Cela permet de garantir l’alignement de certaines variables tant que la structure elle-même est correctement alignée. J’essaie de convertir la mémoire en utilisant toutes sortes d’approches, je déplace l’allocation de grosse stack vers une fonction distincte, mais le résultat en sortie et la corruption de stack sont incorrects. Je commence vraiment à penser de plus en plus que cette énorme fonction rencontre un bogue dans gcc, ce qui est assez étrange, mais en faisant ce casting, je ne peux pas terminer cette chose, peu importe ce que j’essaie. désactivé toutes les optimisations qui nécessitent un alignement, c’est du code C pur, mais j’obtiens de mauvais résultats (les sorties non-bitexact et les corruptions de stack occasionnelles se bloquent). Le correctif simple qui résout tout cela, j’écris au lieu de:

     char buf[sizeof(tmp) + 32]; tmp * X = (tmp*)((((uintptr_t)buf) + 31) & (~31)); 

    ce code:

     tmp buf; tmp * X = &buf; 

    alors tous les bugs disparaissent! Le seul problème est que ce code ne fait pas un alignement correct pour les tableaux et va planter avec les optimisations activées.

    Observation intéressante:
    J’ai mentionné que cette approche fonctionne bien et produit les résultats attendus:

     tmp buf; tmp * X = &buf; 

    Dans un autre fichier, j’ai ajouté une fonction autonome noinline qui lance simplement un pointeur vide sur cette structure tmp *:

     struct tmp * to_struct_tmp(void * buffer32) { return (struct tmp *)buffer32; } 

    Au départ, je pensais que si je lançais de la mémoire allouée à l’aide de to_struct_tmp, il tromperait gcc pour produire des résultats que je m’attendais à obtenir, mais qu’il produisait toujours une sortie non valide. Si j’essaie de modifier le code de travail de cette façon:

     tmp buf; tmp * X = to_struct_tmp(&buf); 

    alors je reçois le même mauvais résultat! WOW, que puis-je dire d’autre? Selon la règle d’aliasing ssortingcte, gcc suppose peut-être que tmp * X n’est pas lié à tmp buf et a supprimé tmp buf tant que variable inutilisée juste après le retour de to_struct_tmp? Ou fait quelque chose d’étrange qui produit un résultat inattendu. J’ai également essayé d’inspecter l’assemblage généré, cependant, en modifiant tmp * X = &buf; to tmp * X = to_struct_tmp(&buf); produit un code extrêmement différent pour la fonction, donc, en quelque sorte, cette règle de repliement affecte énormément la génération de code.

    Conclusion:
    Après toutes sortes de tests, j’ai une idée de la raison pour laquelle je ne peux peut-être pas le faire fonctionner, peu importe ce que j’essaie. Sur la base d’un alias de type ssortingct, GCC pense que le tableau statique est inutilisé et n’alloue donc pas la stack pour celui-ci. Ensuite, les variables locales qui utilisent également la stack sont écrites au même endroit que ma structure tmp ; En d’autres termes, ma structure jumbo partage la même mémoire de stack que d’autres variables de la fonction. Seulement cela pourrait expliquer pourquoi il en résulte toujours le même mauvais résultat. -fno-ssortingct-aliasing résout le problème, comme prévu dans ce cas.

      Désactivez simplement l’optimisation basée sur un alias et appelez cela un jour.

      Si vos problèmes sont réellement causés par des optimisations liées à un alias ssortingct, alors -fno-ssortingct-aliasing résoudra le problème. De plus, dans ce cas, vous n’avez pas à craindre de perdre l’optimisation car, par définition, ces optimisations sont dangereuses pour votre code et vous ne pouvez pas les utiliser.

      Bon point par prétorien . Je me souviens de l’hystérie d’un développeur provoquée par l’introduction de l’parsing des pseudonymes dans gcc. Un certain auteur du kernel Linux voulait (A) aliaser des choses, et (B) continue d’obtenir cette optimisation. (C’est une simplification excessive, mais il semble qu’un -fno-ssortingct-aliasing résoudrait le problème, mais ne coûterait pas cher, et ils devaient tous avoir d’autres poissons à fouetter.)

      Premièrement, je voudrais dire que je suis définitivement avec vous lorsque vous demandez de ne pas parler de “violation standard”, de “mise en œuvre”, etc. Votre question est tout à fait légitime, à mon humble avis.

      Votre approche consistant à regrouper tous les tableaux au sein d’une même struct également un sens, c’est ce que je ferais.

      La formulation de la question ne dit pas clairement quels “artefacts” observez-vous. Existe-t-il du code inutile? Ou un mauvais alignement des données? Si tel est le cas, vous pouvez (espérons-le) utiliser des éléments tels que STATIC_ASSERT pour vous assurer, au moment de la compilation, que les éléments sont correctement alignés. Ou au moins avoir un ASSERT au moment de la ASSERT du débogage.

      Comme Eric Postpischil l’a proposé, vous pouvez envisager de déclarer cette structure comme étant globale (si cela s’applique au cas, je veux dire que le multi-threading et la récursivité ne sont pas une option).

      Un autre point que j’aimerais remarquer est ce qu’on appelle les sondes de stack. Lorsque vous allouez beaucoup de mémoire de la stack à une seule fonction (plus d’une page pour être exacte) – sur certaines plates-formes (telles que Win32), le compilateur ajoute un code d’initialisation supplémentaire, appelé sondes de stack. Cela peut également avoir un impact sur les performances (même s’il est susceptible d’être mineur).

      En outre, si vous n’avez pas besoin de tous les 40 tableaux simultanément, vous pouvez en organiser certains dans une union . C’est-à-dire que vous aurez une grande struct , à l’intérieur de laquelle certaines sous- structs seront regroupées en une union .

      Il y a un certain nombre de problèmes ici.

      Alignement: Il y a peu de choses qui nécessitent un alignement sur 32 octets. L’alignement sur 16 octets est avantageux pour les types SIMD sur les processeurs Intel et ARM actuels. Avec AVX sur les processeurs Intel actuels, le coût de performance lié à l’utilisation d’adresses alignées sur 16 octets mais non alignées sur 32 octets est généralement modéré. Les magasins de 32 octets traversant une ligne de cache peuvent être soumis à une pénalité importante; un alignement de 32 octets peut donc être utile. Sinon, un alignement sur 16 octets peut suffire. (Sous OS X et iOS, malloc renvoie une mémoire alignée sur 16 octets.)

      Allocation dans un code critique: évitez d’allouer de la mémoire dans un code essentiel à la performance. En règle générale, la mémoire doit être allouée au début du programme ou avant le début du travail critique pour les performances, et réutilisée pendant le code critique pour les performances. Si vous allouez de la mémoire avant le début du code de performances critiques, le temps nécessaire à l’allocation et à la préparation de la mémoire n’a plus d’importance.

      Grandes et nombreuses baies sur la stack: La stack n’est pas conçue pour des allocations de mémoire importantes et son utilisation est limitée. Même si vous ne rencontrez pas de problèmes maintenant, des modifications apparemment non liées de votre code pourraient interagir avec l’utilisation de beaucoup de mémoire sur la stack et provoquer des débordements de stack.

      De nombreux tableaux: 40 tableaux, c’est beaucoup. À moins qu’ils ne soient tous utilisés simultanément pour différentes données, vous devez impérativement chercher à réutiliser une partie du même espace pour des données et des objectives différents. L’utilisation inutile de baies différentes peut entraîner plus de destruction de cache que nécessaire.

      Optimisation: vous ne comprenez pas ce que vous entendez par «le désordre de l’alignement confond l’optimiseur et que l’allocation de registres différente ralentit considérablement la fonction». Si vous avez plusieurs tableaux automatiques dans une fonction, je m’attendrais généralement à ce que l’optimiseur sache qu’ils sont différents, même si vous dérivez des pointeurs à partir des tableaux par arithmétique d’adresse. Par exemple, un code donné tel a[i] = 3; b[i] = c[i]; a[i] = 4; a[i] = 3; b[i] = c[i]; a[i] = 4; , Je pense que l’optimiseur saura que a , b et c sont des tableaux différents et que, par conséquent, c[i] ne peut pas être identique à a[i] , il est donc normal d’éliminer a[i] = 3; . Peut-être avez-vous un problème: avec 40 tableaux, vous avez 40 pointeurs vers des tableaux, de sorte que le compilateur finit par déplacer des pointeurs dans des registres?

      Dans ce cas, la réutilisation de moins de baies de stockage à plusieurs fins peut aider à réduire cela. Si vous avez un algorithme qui utilise en réalité 40 tableaux à la fois, vous pouvez envisager de le restructurer pour qu’il utilise moins de tableaux à la fois. Si un algorithme doit pointer sur 40 emplacements différents en mémoire, vous avez besoin de 40 pointeurs, quel que soit leur emplacement ou leur mode d’affectation, et 40 points sont plus que les registres disponibles.

      Si vous avez d’autres préoccupations concernant l’optimisation et l’utilisation des registres, vous devriez être plus précis à ce sujet.

      Crénelage et artefacts: vous signalez des problèmes de crénelage et d’artefact, mais vous ne donnez pas suffisamment de détails pour les comprendre. Si vous avez un grand tableau de caractères que vous réinterprétez comme une structure contenant tous vos tableaux, il n’y a pas d’alias dans la structure. Les problèmes que vous rencontrez ne sont donc pas clairs.

      L’alignement sur 32 octets sonne comme si vous poussiez le bouton trop loin. Aucune instruction de la CPU ne devrait nécessiter un alignement aussi grand. En gros, un alignement aussi large que le type de données le plus large de votre architecture devrait suffire.

      C11 a le concept de maxalign_t , qui est un type fictif d’alignement maximal pour l’architecture. Si votre compilateur ne l’a pas encore, vous pouvez facilement le simuler avec quelque chose comme:

       union maxalign0 { long double a; long long b; ... perhaps a 128 integer type here ... }; typedef union maxalign1 maxalign1; union maxalign1 { unsigned char bytes[sizeof(union maxalign0)]; union maxalign0; } 

      Vous avez maintenant un type de données qui a l’alignement maximal de votre plate-forme et qui est initialisé par défaut avec tous les octets définis sur 0 .

       maxalign1 history_[someSize]; short * history = history_.bytes; 

      Cela évite les calculs d’adresse horribles que vous faites actuellement, il vous someSize de faire quelque adoption de someSize pour prendre en compte que vous sizeof(maxalign1) toujours des multiples de sizeof(maxalign1) .

      Assurez-vous également que cela ne pose aucun problème de repliement. Tout d’abord, les unions en C créées à cet effet, puis les pointeurs de caractère (de toute version) sont toujours autorisés à aliaser tout autre pointeur.