aliasing strict et alignement de la mémoire

j'ai du code de performance critique et il y a une fonction énorme qui affecte comme 40 tableaux de différentes tailles sur la pile au début de la fonction. La plupart de ces tableaux doivent avoir un certain alignement (parce que ces tableaux sont accessibles ailleurs dans la chaîne en utilisant des instructions cpu qui nécessitent un alignement de la mémoire (pour les processeurs Intel et arm).

car certaines versions de gcc ne parviennent tout simplement pas à aligner correctement les variables de la pile (notamment pour le code arm), ou même parfois il dit que l'alignement maximum pour l'architecture cible est inférieur à ce que mon code demande réellement, je n'ai tout simplement pas d'autre choix que d'allouer ces tableaux sur la pile et de les aligner manuellement.

Donc, pour chaque tableau, j'besoin de faire quelque chose comme ça pour obtenir aligné correctement:

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

de Cette façon, history est maintenant aligné sur la limite de 32 octets. Faire la même chose est fastidieux pour tous les 40 tableaux, plus cette partie du code est vraiment intensif cpu et je ne peux tout simplement pas faire le même technique d'alignement pour chacun des tableaux (ce désordre d'alignement confond l'optimiseur et l'allocation de Registre différente ralentit la fonction grand temps, pour une meilleure explication voir explication à la fin de la question).

... évidemment, je veux faire un alignement manuel une seule fois, et supposons que ces tableaux sont situés l'un après l'autre. J'ai aussi ajouté du rembourrage supplémentaire à ces tableaux pour qu'ils soient toujours multiples de 32 octets. Donc, puis-je tout simplement de créer un jumbo char tableau sur la pile et la convertir en une structure qui dispose de tous ces aligné les tableaux:

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 ça. Peut-être pas le plus élégant, mais il a produit un très bon résultat 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 construction a été mis à jour pour utiliser de nouveaux GCC et soudainement nous avons commencé à avoir quelques Artéfacts dans les données générées (par exemple, la sortie de la suite de test de validation n'est plus un peu exacte même en C pur construire avec les asm code). Il a fallu beaucoup de temps pour déboguer la question et il semble être liée aux règles d'Alias et les nouvelles versions de GCC.

Donc, comment puis-je le faire? S'il vous plaît, ne perdez pas de temps à essayer d'expliquer que ce n'est pas standard, pas portable, Non défini etc (j'ai lu de nombreux articles à ce sujet). En outre, il n'y a aucun moyen que je puisse changer le code (je voudrais peut-être envisager de modifier GCC aussi bien pour corriger le problème, mais pas le remaniement du code)... fondamentalement, tout ce que je veux est d'appliquer un sort de magie noire pour que le nouveau GCC produise le même code fonctionnel pour ce type de code sans désactiver les optimisations?

Edit:

  • j'ai utilisé ce code sur plusieurs os/compilateurs, mais j'ai commencé à avoir des problèmes quand je suis passé au nouveau NDK qui est basé sur GCC 4.6. J'obtiens le même mauvais résultat avec GCC 4.7 (de NDK r8d)
  • je mentionne 32 octets de l'alignement. Si elle fait mal aux yeux, de la remplacer par une autre le nombre que vous aimez, par exemple 666 si cela aide. Il est absolument inutile de mentionner que la plupart des architectures n'ont pas besoin de cet alignement. Si j'ai aligné 8KO de tableaux locaux sur la pile, je perds 15 octets pour l'alignement de 16 octets et je perds 31 octets pour l'alignement de 32 octets. J'espère que c'est clair ce que je veux dire.

  • je dis qu'il y a comme 40 tableaux sur la pile dans le code de performance critique. Je dois probablement aussi dire que c'est un tiers Vieux code qui a bien fonctionné et je ne veux pas jouer avec ça. Pas besoin de dire si c'est bon ou mauvais, ça ne sert à rien.

  • ce code/fonction a un comportement bien testé et défini. Nous avons des nombres exacts des exigences de ce code par exemple il alloue Xkb ou RAM, utilise Y kb des tables statiques, et consomme jusqu'à Z kb de l'espace de pile et il ne peut pas changer, puisque le code ne sera pas changé.

  • en disant que "le désordre d'alignement confond l'optimiseur" je veux dire que si je essayez d'aligner chaque tableau séparément code optimizer attribue des registres supplémentaires pour le code d'alignement et les parties critiques de la performance du code soudainement n'ont pas assez de registres et commencent à se détruire pour empiler à la place ce qui entraîne un ralentissement du code. Ce comportement a été observé sur les processeurs ARM (Je ne m'inquiète pas du tout pour intel).

  • Par des artefacts, je voulais dire que la sortie devient non-bitexact, il y a du bruit ajouté. Soit à cause de ce type d'aliasing problème ou un bug du compilateur que les résultats finalement dans la mauvaise sortie de la fonction.

    En bref, le point de la question... Comment puis-je attribuer une quantité aléatoire d'espace de pile (en utilisant des tableaux de char ou alloca, puis aligner le pointeur sur l'espace de la pile et réinterpréter ce morceau de mémoire comme une structure qui a une disposition bien définie qui garantit l'alignement de certaines variables tant que la structure elle-même est alignée correctement. J'essaie de lancer la mémoire en utilisant toutes sortes d'approches, je déplace l'allocation de la grande pile vers une fonction séparée, mais j'obtiens de mauvaises sorties et de la corruption de la pile, je commence vraiment à penser de plus en plus que cette fonction énorme touche une sorte de bogue dans gcc. C'est assez étrange, qu'en faisant ce plâtre Je ne puisse pas faire cette chose peu importe ce que j'essaie. Au fait, j'ai désactivé toutes les optimisations qui nécessitent un alignement, c'est du code de style C pur maintenant, mais j'obtiens de mauvais résultats (sortie non-bitexact et corruptions occasionnelles de la pile) accident.) La seule solution qui résout tout, 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;
    

    puis tous les insectes disparaissent! Le seul problème est que ce code ne fait pas l'alignement approprié pour les tableaux et va se planter avec des optimisations activées.

    observation intéressante:

    Je l'ai mentionné que cette approche fonctionne bien et produit le résultat attendu:

    tmp buf;
    tmp * X = &buf;
    

    Dans un autre fichier, j'ai ajouté un fonction noinline autonome qui envoie simplement un pointeur de vide à cette structure tmp*:

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

    au départ, j'ai pensé que si je jetais de la mémoire alloc'Ed en utilisant to_struct_tmp, cela pousserait gcc à produire des résultats que je m'attendais à obtenir, mais cela produirait quand même une sortie invalide. Si j'essaie de modifier le code de travail de cette façon:

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

    puis-je obtenir la même mauvais la suite!!! WOW, ce que je peux dire? Peut-être, basé sur la règle d'alias strict gcc suppose que tmp * X n'est pas lié à une tmp buf et supprimé tmp buf comme 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 aussi essayé d'inspecter l'assemblage généré, cependant, changer tmp * X = &buf;tmp * X = to_struct_tmp(&buf); produit un code extrêmement différent pour la fonction, donc, d'une certaine façon, cette règle d'alias affecte la génération de code à grande échelle.

    Conclusion:

    Après toutes sortes de tests, j'ai une idée pourquoi je ne peux pas le faire fonctionner, peu importe ce que j'essaie. Basé sur un aliasing de type strict, GCC pense que le tableau statique n'est pas utilisé et ne lui attribue donc pas de pile. Ensuite, les variables locales qui utilisent aussi stack sont écrites au même endroit où my tmp struct est stocké; en d'autres termes, my jumbo struct partage la même mémoire de pile que les autres variables de la fonction. Seul cela pourrait expliquer pourquoi il en résulte toujours le même mauvais résultat. -FNO-strict-aliasing corrige le problème, comme prévu dans ce cas.

  • 17
    demandé sur Pavel 2013-01-05 12:23:19

    4 réponses

    il suffit de désactiver l'optimisation basée sur un alias et de l'appeler un jour

    si vos problèmes sont en fait causés par des optimisations liées à un aliasing strict, alors -fno-strict-aliasing résoudra le problème. De plus, dans ce cas, vous n'avez pas à vous soucier de perdre l'optimisation parce que,par définition, ces optimisations sont dangereux pour votre code et vous vous ne pouvez pas utiliser.

    Bon point par Prétorienne. Je me souviens de l'hystérie d'un promoteur. invité par l'introduction de l'analyse d'alias dans gcc. Un certain auteur de noyau Linux voulait (a) des choses d'alias, et (b) toujours obtenir cette optimisation. (C'est une simplification excessive mais il semble que -fno-strict-aliasing résoudraient le problème, ne coûteraient pas beaucoup, et ils ont tous dû avoir d'autres poissons à frire.)

    4
    répondu DigitalRoss 2017-05-23 12:00:44

    tout d'abord, je voudrais dire que je suis définitivement avec vous quand vous demandez de ne pas parler de "violation standard", "dépendant de la mise en œuvre" et ainsi de suite. Votre question est absolument légitime IMHO.

    votre approche pour empaqueter tous les tableaux dans un seul struct c'est aussi logique, c'est ce que je ferais.

    d'après la formulation de la question, il n'est pas clair quels "artefacts" observez-vous. Y a-t-il du code non nécessaire généré? De données ou de désalignement? Si ce dernier est le cas vous pouvez (avec un peu de chance) utilisez des choses comme STATIC_ASSERT pour s'assurer au moment de la compilation que les choses sont alignées correctement. Ou au moins certains d'exécution ASSERT au debug.

    comme Eric Postpischil l'a proposé, vous pouvez envisager de déclarer cette structure comme globale (si cela s'applique au cas, je veux dire multi-threading et recursion ne sont pas une option).

    un autre point que j'aimerais remarquer est ce qu'on appelle des sondes à pile. Quand vous allouez beaucoup de mémoire à partir de la pile dans une seule fonction (plus que 1 page pour être exact) - sur certaines plates-formes (Win32) le compilateur ajoute un code d'initialisation supplémentaire, appelée pile sondes. Cela peut aussi avoir un certain impact sur le rendement (bien que probablement mineur).

    en outre, si vous n'avez pas besoin de tous les 40 tableaux simultanément vous pouvez organiser certains d'entre eux dans un union. Qui est, vous aurez un grand struct à l'intérieur de laquelle certains sous-structs sera regroupé en union.

    3
    répondu valdo 2013-01-05 13:31:40

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

    Alignement: il y a peu de choses qui nécessitent un alignement de 32 octets. L'alignement de 16 octets est bénéfique pour les types SIMD sur les processeurs Intel et ARM courants. Avec AVX sur les processeurs Intel actuels, le coût de performance de l'utilisation d'adresses alignées sur 16 octets mais pas sur 32 octets est généralement faible. Il peut y avoir une pénalité importante pour les magasins de 32 octets qui traversent une ligne de cache, donc l'alignement de 32 octets peut être utile là-bas. Autrement, Un alignement de 16 octets peut être acceptable. (On OS X and iOS,malloc renvoie une mémoire alignée de 16 octets.)

    Allocation en code critique: vous devriez éviter d'allouer de la mémoire dans le code de performance critique. En général, la mémoire devrait être attribuée au début du programme, ou avant le début du travail critique pour l'exécution, et réutilisée pendant le code critique pour l'exécution. Si vous allouez de la mémoire avant que le code critique de performance commence, alors le temps qu'il faut pour allouer et préparer le la mémoire est essentiellement hors de propos.

    Grand, de nombreux tableaux sur la pile: la pile n'est pas destinée aux grandes attributions de mémoire, et il y a des limites à son utilisation. Même si vous ne rencontrez PAS de problèmes maintenant, des changements apparemment sans rapport dans votre code dans le futur pourraient interagir avec l'utilisation d'une grande quantité de mémoire sur la pile et causer des débordements de pile.

    de Nombreux tableaux: 40 tableaux, c'est beaucoup. À moins que ceux-ci soient tous utilisés pour différents les données en même temps, et forcément, vous devriez chercher à réutiliser le même espace pour des données différentes et fins. L'utilisation inutile de différents tableaux peut causer plus de casse de cache que nécessaire.

    Optimisation: il n'est pas clair ce que vous voulez dire en disant que le "désordre d'alignement confond l'optimiseur et l'allocation de Registre différente ralentit la fonction grand temps". Si vous avez plusieurs tableaux automatiques à l'intérieur d'une fonction, je m'attendrais généralement l'optimiseur pour savoir qu'ils sont différents, même si vous dérivez des pointeurs des tableaux par arithmétique d'adresse. Par exemple, code donné tel que a[i] = 3; b[i] = c[i]; a[i] = 4;, je m'attends à ce que l'optimiseur de savoir que a, b et c sont des tableaux différents, et donc c[i] ne peut pas être le même que a[i], de sorte qu'il est d'accord pour éliminer a[i] = 3;. Peut-être un problème que vous avez est que, avec 40 tableaux, vous avez 40 pointeurs à tableaux, de sorte que le compilateur finit par déplacer des pointeurs dans et hors de les registres?

    dans ce cas, réutiliser moins de matrices à des fins multiples pourrait aider à réduire cela. Si vous avez un algorithme qui utilise en fait 40 tableaux à la fois, alors vous pourriez envisager de restructurer l'algorithme de sorte qu'il utilise moins de tableaux à la fois. Si un algorithme doit pointer à 40 endroits différents dans la mémoire, alors vous avez essentiellement besoin de 40 pointeurs, indépendamment de l'endroit ou de la façon dont ils sont alloués, et 40 pointeurs est plus qu'il n'y a de registres disponibles.

    si vous avez d'autres préoccupations concernant l'optimisation et l'enregistrement de l'utilisation, vous devriez être plus précis à ce sujet.

    Aliasing et des artefacts: vous signalez qu'il y a des problèmes d'Alias et d'artefacts, mais vous ne donnez pas suffisamment de détails pour les comprendre. Si vous avez un gros char tableau que vous réinterprétez comme une structure contenant tous vos tableaux, alors il n'y a pas d'aliasing dans la structure. Il n'est donc pas clair quels sont les problèmes que vous rencontrez.

    2
    répondu Eric Postpischil 2013-01-05 13:04:11

    32 l'alignement des octets sonne comme si vous poussiez le bouton trop loin. Aucune instruction CPU ne devrait exiger un alignement aussi grand que celui-là. Fondamentalement, un alignement aussi large que le plus grand type de données de votre architecture devrait suffire.

    C11 a la notion fo maxalign_t, qui est un type de mannequin d'alignement maximum pour l'architecture. Si votre compilateur ne l'a pas, encore, vous pouvez facilement simuler par 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;
    }
    

    Maintenant, vous avez un type de données qui a l'alignement maximum de votre plate-forme et qui est initialisé par défaut avec tous les octets sur 0.

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

    cela évite les horribles calculs d'adresses que vous faites actuellement, vous n'aurez qu'à faire quelques adoptions de someSize pour tenir compte du fait que vous attribuez toujours des multiples de sizeof(maxalign1).

    soyez également assurés que cela n'a pas de problèmes d'alias. Tout d'abord unions en C fait pour cela, et puis les pointeurs de caractères (de n'importe quelle version) sont toujours autorisés à alias tout autre pointeur.

    1
    répondu Jens Gustedt 2013-01-05 08:54:05