(N'appliquez pas ces règles sans réfléchir. Voir la remarque d'ESR sur la localisation du cache pour les membres que vous utilisez ensemble. Et dans les programmes multithreads, attention au faux partage des membres écrits par des threads différents. En général, vous ne voulez pas du tout de données par thread dans une structure unique pour cette raison, à moins que vous ne le fassiez pour contrôler la séparation avec une grande structure de type alignas(128)
. Ceci s'applique à atomic
et les variables non atomiques ; ce qui compte, ce sont les threads qui écrivent dans les lignes de cache, quelle que soit la manière dont ils le font).
Règle de base : du plus grand au plus petit alignof()
. Il n'y a rien que vous puissiez faire qui soit parfait partout, mais de loin le cas le plus courant de nos jours est une implémentation saine et "normale" de C++ pour un CPU 32 ou 64 bits normal. Tous les types primitifs ont des tailles en puissance de 2.
La plupart des types ont alignof(T) = sizeof(T)
ou alignof(T)
plafonné à la largeur de registre de l'implémentation. Ainsi, les grands types sont généralement plus alignés que les petits types.
Les règles d'empaquetage des structures dans la plupart des ABIs donnent aux membres de la structure leur valeur absolue. alignof(T)
par rapport au début de la structure, et la structure elle-même hérite de l'alignement le plus large. alignof()
de l'un de ses membres.
-
Privilégier les membres toujours 64 bits (comme double
, long long
y int64_t
). Bien sûr, l'ISO C++ ne fixe pas ces types à 64 bits / 8 octets, mais en pratique, sur tous les processeurs dont vous vous souciez, ils le sont. Les personnes qui portent votre code vers des processeurs exotiques peuvent modifier la disposition des structures pour les optimiser si nécessaire.
-
alors les pointeurs et des entiers de largeur de pointeur : size_t
, intptr_t
y ptrdiff_t
(qui peut être de 32 ou 64 bits). Ce sont toutes les mêmes largeurs sur les implémentations modernes normales de C++ pour les CPUs avec un modèle de mémoire plat.
Pensez à mettre les pointeurs gauche/droite de la liste chaînée et de l'arbre en premier si vous vous intéressez aux processeurs x86 et Intel. Pointer à travers les nœuds d'un arbre ou d'une liste chaînée a des pénalités lorsque l'adresse de départ de la structure se trouve dans une page 4k différente de celle du membre auquel vous accédez. . Les faire passer en premier garantit que cela ne peut pas être le cas.
-
puis long
(qui est parfois 32 bits même si les pointeurs sont 64 bits, dans les ABI LLP64 comme Windows x64). Mais il est garanti au moins aussi large que int
.
-
puis 32 bits int32_t
, int
, float
, enum
. (En option, séparer int32_t
y float
devant int
si vous vous intéressez à d'éventuels systèmes 8 / 16 bits qui remplissent encore ces types en 32 bits, ou qui font mieux avec un alignement naturel. La plupart de ces systèmes n'ont pas de charges plus larges (FPU ou SIMD), donc les types plus larges doivent être traités comme de multiples morceaux séparés tout le temps de toute façon).
L'ISO C++ permet int
peut être aussi étroite que 16 bits, ou arbitrairement large, mais en pratique, c'est un type 32 bits, même sur les CPU 64 bits. Les concepteurs de l'ABI ont constaté que les programmes conçus pour fonctionner avec des processeurs 32-bit int
ne font que gaspiller de la mémoire (et de l'empreinte de cache) si int
était plus large. Ne faites pas d'hypothèses qui poseraient des problèmes d'exactitude, mais pour des "performances portables", il faut juste avoir raison dans le cas normal.
Les personnes qui mettent au point votre code pour des plates-formes exotiques peuvent le modifier si nécessaire. Si la disposition d'une certaine structure est critique sur le plan des performances, vous pouvez commenter vos hypothèses et votre raisonnement dans l'en-tête.
-
puis short
/ int16_t
-
puis char
/ int8_t
/ bool
-
(pour de multiples bool
surtout s'ils sont lus majoritairement ou s'ils sont tous modifiés ensemble, pensez à les emballer avec des champs de bits de 1 bit).
(Pour les types d'entiers non signés, trouvez le type signé correspondant dans ma liste).
Un multiple de 8 octets tableau de types plus étroits peut aller plus tôt si vous le souhaitez. Mais si vous ne connaissez pas les tailles exactes des types, vous ne pouvez pas garantir que les types les plus étroits peuvent partir plus tôt si vous le souhaitez. int i
+ char buf[4]
remplira un emplacement de 8 octets alignés entre deux double
s. Mais ce n'est pas une mauvaise hypothèse, donc je le ferais quand même s'il y avait une raison (comme la localité spatiale des membres accédés ensemble) de les mettre ensemble plutôt qu'à la fin.
Types exotiques : Le Système V x86-64 a alignof(long double) = 16
mais i386 System V n'a que alignof(long double) = 4
, sizeof(long double) = 12
. Il s'agit du type x87 80 bits, qui est en fait de 10 octets, mais dont la taille est portée à 12 ou 16 afin d'être un multiple de son alignof, ce qui rend les tableaux possibles sans violer la garantie d'alignement.
Et en général cela devient plus délicat lorsque les membres de votre structure sont eux-mêmes des agrégats (struct ou union) avec une fonction sizeof(x) != alignof(x)
.
Une autre particularité est que dans certains ABI (par exemple Windows 32 bits si je me souviens bien), les membres de la structure sont alignés sur leur taille (jusqu'à 8 octets). par rapport au début de la structure même si alignof(T)
n'est toujours que de 4 pour double
y int64_t
.
Cela permet d'optimiser le cas courant d'allocation séparée de mémoire alignée sur 8 octets pour une structure unique, sans donner d'alignement. garantie . Le Système V i386 a également la même alignof(T) = 4
pour la plupart des types primitifs (mais malloc
vous donne toujours une mémoire alignée sur 8 octets parce que alignof(maxalign_t) = 8
). Mais de toute façon, le système V de i386 n'a pas cette règle d'empaquetage des structures, donc (si vous ne disposez pas votre structure du plus grand au plus petit) vous pouvez vous retrouver avec des membres de 8 octets sous-alignés par rapport au début de la structure.
La plupart des processeurs ont des modes d'adressage qui, à partir d'un pointeur dans un registre, permettent d'accéder à n'importe quel décalage d'octet. Le décalage maximal est généralement très grand, mais sur x86, il permet d'économiser de la taille de code si le décalage d'octet tient dans un octet signé ( [-128 .. +127]
). Ainsi, si vous avez un grand tableau, quel qu'il soit, préférez le placer plus tard dans la structure. après les membres les plus utilisés. Même si cela coûte un peu de rembourrage.
Votre compilateur créera presque toujours un code dont l'adresse de la structure se trouve dans un registre, et non au milieu de la structure, afin de tirer parti des déplacements négatifs courts.
Eric S. Raymond a écrit un article L'art perdu de l'emballage structuré . En particulier, la section sur Réorganisation de la structure est en fait une réponse à cette question.
Il soulève également un autre point important :
9. Lisibilité et localisation du cache
Alors que le réordonnancement par taille est le moyen le plus simple d'éliminer le désordre, ce n'est pas nécessairement la bonne chose . Il y a deux autres problèmes : la lisibilité et la localisation du cache.
Dans un grand site structure qui peut facilement être divisée à travers une frontière de ligne de cache, il est logique de mettre 2 choses à proximité si elles sont toujours utilisées ensemble. Ou même contiguës pour permettre la coalescence du chargement/stockage, par exemple en copiant 8 ou 16 octets avec un entier (non aligné) ou un chargement/stockage SIMD au lieu de charger séparément les plus petits membres.
Les lignes de cache sont généralement de 32 ou 64 octets sur les processeurs modernes. (Sur les x86 modernes, toujours 64 octets. Et la famille Sandybridge dispose d'un préchargeur spatial de lignes adjacentes dans le cache L2 qui tente de compléter des paires de lignes de 128 octets, séparément du flux principal L2 HW prefetch pattern detector et L1d prefetching).
Fait amusant : Rust permet au compilateur de réorganiser les structures pour un meilleur empaquetage, ou pour d'autres raisons. Je ne sais pas si un compilateur le fait réellement, cependant. Probablement seulement possible avec l'optimisation du programme entier au moment de la liaison si vous voulez que le choix soit basé sur la façon dont la structure est réellement utilisée. Sinon, les parties du programme compilées séparément ne pourraient pas se mettre d'accord sur une disposition.
(@alexis a posté une réponse en lien uniquement vers l'article de l'ESR, donc merci pour ce point de départ).