web-dev-qa-db-fra.com

Qu'est-ce que chaque programmeur devrait savoir sur la mémoire?

Je me demande quelle quantité d'Ulrich Drepper Ce que chaque programmeur devrait savoir sur la mémoire de 2007 est toujours valide. De plus, je n'ai pas pu trouver de version plus récente que 1.0 ou un errata.

133
Framester

Pour autant que je me souvienne, le contenu de Drepper décrit les concepts fondamentaux de la mémoire: comment fonctionne le cache CPU, qu'est-ce que la mémoire physique et virtuelle et comment le noyau Linux gère ce zoo. Il y a probablement des références d'API obsolètes dans certains exemples, mais cela n'a pas d'importance; cela n'affectera pas la pertinence des concepts fondamentaux.

Ainsi, tout livre ou article décrivant quelque chose de fondamental ne peut pas être considéré comme dépassé. "Ce que tout programmeur doit savoir sur la mémoire" vaut vraiment la peine d'être lu, mais je ne pense pas que ce soit pour "chaque programmeur". Il est plus adapté aux gars système/embarqué/noyau.

93
Dan Kruchinin

Le guide au format PDF se trouve à https://www.akkadia.org/drepper/cpumemory.pdf .

Il est toujours généralement excellent et fortement recommandé (par moi, et je pense par d'autres experts en optimisation des performances). Ce serait cool si Ulrich (ou n'importe qui d'autre) écrivait une mise à jour 2017, mais ce serait beaucoup de travail (par exemple, relancer les benchmarks). Voir aussi d'autres liens d'optimisation des performances x86 et d'optimisation SSE/asm (et C/C++) dans x86tag wiki . (L'article d'Ulrich n'est pas spécifique à x86, mais la plupart (tous) ses benchmarks sont sur du matériel x86.)

Les détails matériels de bas niveau sur le fonctionnement de la DRAM et des caches s'appliquent toujours . DDR4 utilise les mêmes commandes comme décrit pour DDR1/DDR2 (lecture/écriture en rafale). Les améliorations DDR3/4 ne sont pas des changements fondamentaux. AFAIK, toutes les choses indépendantes d'Arch s'appliquent toujours de manière générale, par exemple à AArch64/ARM32.

Voir aussi la section Plateformes liées à la latence de cette réponse pour des détails importants sur l'effet de la mémoire/latence L3 sur le thread unique bande passante: bandwidth <= max_concurrency / latency, et il s'agit en fait du principal goulot d'étranglement pour la bande passante à thread unique sur un processeur multicœur moderne comme un Xeon. Mais un bureau Skylake quad-core peut s'approcher de la maximisation de la bande passante DRAM avec un seul thread. Ce lien contient de très bonnes informations sur les magasins NT par rapport aux magasins normaux sur x86. Pourquoi Skylake est-il tellement meilleur que Broadwell-E pour le débit de mémoire à thread unique? est un résumé.

Ainsi, la suggestion d'Ulrich dans 6.5.8 Utilisation de toute la bande passante concernant l'utilisation de la mémoire distante sur d'autres nœuds NUMA ainsi que le vôtre, est contre-productive sur du matériel moderne où les contrôleurs de mémoire ont plus de bande passante qu'un seul cœur peut utiliser. Eh bien, vous pouvez peut-être imaginer une situation où il y a un avantage net à exécuter plusieurs threads gourmands en mémoire sur le même nœud NUMA pour une communication inter-threads à faible latence, mais en les faisant utiliser la mémoire distante pour des trucs sensibles à la latence à large bande passante. Mais c'est assez obscur, il suffit normalement de diviser les threads entre les nœuds NUMA et de leur faire utiliser la mémoire locale. La bande passante par cœur est sensible à la latence en raison des limites de concurrence maximale (voir ci-dessous), mais tous les cœurs d'un socket peuvent généralement plus que saturer les contrôleurs de mémoire de ce socket.


(généralement) N'utilisez pas le logiciel de prélecture

Une chose importante qui a changé est que la pré-lecture matérielle est beaucoup meilleure que sur le Pentium 4 et peut reconnaître les modèles d'accès stridé jusqu'à une foulée assez importante, et plusieurs flux à la fois (par exemple un avant/arrière par page de 4k). Manuel d'optimisation d'Intel décrit certains détails des pré-récupérateurs HW dans différents niveaux de cache pour leur microarchitecture de la famille Sandybridge. Ivybridge et les versions ultérieures ont une prélecture matérielle de la page suivante, au lieu d'attendre un échec de cache dans la nouvelle page pour déclencher un démarrage rapide. Je suppose qu'AMD contient des informations similaires dans son manuel d'optimisation. Attention, le manuel d'Intel regorge également d'anciens conseils, dont certains ne sont valables que pour P4. Les sections spécifiques à Sandybridge sont bien sûr précises pour SnB, mais par ex. n-laminage des uops micro-fondus changé dans HSW et le manuel ne le mentionne pas .

Le conseil habituel de nos jours est de supprimer tous les prélecture SW de l'ancien code , et ne pensez à le remettre si le profilage montre des échecs de cache (et vous êtes pas saturer la bande passante mémoire). La prélecture des deux côtés de l'étape suivante d'une recherche binaire peut encore aider. par exemple. une fois que vous avez décidé quel élément regarder ensuite, pré-récupérez les éléments 1/4 et 3/4 afin qu'ils puissent se charger en parallèle avec le milieu de chargement/vérification.

La suggestion d'utiliser un thread de prélecture séparé (6.3.4) est totalement obsolète , je pense, et n'a jamais été bonne que sur Pentium 4. P4 avait un hyperthreading (2 cœurs logiques partageant un cœur physique), mais pas suffisamment de cache de trace (et/ou de ressources d'exécution hors service) pour gagner en débit en exécutant deux threads de calcul complets sur le même cœur. Mais les processeurs modernes (famille Sandybridge et Ryzen) sont beaucoup plus costauds et devraient soit exécuter un vrai thread soit ne pas utiliser d'hyperthreading (laisser l'autre noyau logique inactif pour que le thread solo dispose de toutes les ressources au lieu de partitionnement du ROB).

La prélecture du logiciel a toujours été "fragile" : les bons numéros de réglage magique pour obtenir une accélération dépendent des détails du matériel, et peut-être de la charge du système. Trop tôt et il est expulsé avant la demande. Trop tard et ça n'aide pas. Cet article de blog montre du code + des graphiques pour une expérience intéressante d'utilisation de la prélecture SW sur Haswell pour la prélecture de la partie non séquentielle d'un problème. Voir aussi Comment utiliser correctement les instructions de prélecture? . NT prefetch est intéressant, mais encore plus fragile car une expulsion précoce de L1 signifie que vous devez aller jusqu'à L3 ou DRAM, pas seulement L2. Si vous avez besoin de chaque dernière baisse de performances, et vous pouvez régler pour une machine spécifique, la prélecture SW vaut la peine d'être examinée pour un accès séquentiel, mais cela mai toujours être un ralentissement si vous avez suffisamment de travail ALU à faire tout en vous rapprochant d'un goulot d'étranglement sur la mémoire.


La taille de la ligne de cache est toujours de 64 octets. (La bande passante de lecture/écriture de L1D est très élevée, et les processeurs modernes peuvent effectuer 2 chargements vectoriels par horloge + 1 magasin de vecteurs si tout se produit dans L1D. Voir Comment le cache peut-il être aussi rapide? .) Avec AVX512, la taille de la ligne = la largeur du vecteur, vous pouvez donc charger/stocker une ligne de cache entière en une seule instruction. Ainsi, chaque chargement/stockage mal aligné traverse une limite de ligne de cache, au lieu de tous les autres pour 256b AVX1/AVX2, ce qui souvent ne ralentit pas le bouclage sur un tableau qui n'était pas dans L1D.

Les instructions de chargement non alignées n'ont aucune pénalité si l'adresse est alignée au moment de l'exécution, mais les compilateurs (en particulier gcc) font un meilleur code lors de l'autovectorisation s'ils connaissent les garanties d'alignement. En fait, les opérations non alignées sont généralement rapides, mais les sauts de page sont toujours douloureux (bien moins sur Skylake, cependant; seulement ~ 11 cycles de latence supplémentaires contre 100, mais toujours une pénalité de débit).


Comme Ulrich l'avait prédit, chaque système multi-socket est NUMA de nos jours: les contrôleurs de mémoire intégrés sont standard, c'est-à-dire qu'il n'y a pas de Northbridge externe. Mais SMP ne signifie plus multi-socket, car les processeurs multi-cœurs sont répandus. Les processeurs Intel de Nehalem à Skylake ont utilisé un grand cache - inclus L3 comme filet de sécurité pour la cohérence entre les cœurs. Les processeurs AMD sont différents, mais je ne suis pas aussi clair sur les détails.

Skylake-X (AVX512) n'a plus de L3 inclus, mais je pense qu'il y a toujours un répertoire de balises qui lui permet de vérifier ce qui est mis en cache n'importe où sur la puce (et si oui où) sans réellement diffuser des snoops sur tous les cœurs. SKX utilise un maillage plutôt qu'un bus en annea , avec une latence généralement encore pire que les précédents Xeons à plusieurs cœurs, malheureusement.

Fondamentalement, tous les conseils sur l'optimisation du placement de la mémoire s'appliquent toujours, seuls les détails de ce qui se passe exactement lorsque vous ne pouvez pas éviter les ratés du cache ou les conflits varient.


6.4.2 Opérations atomiques : l'indice de référence montrant une boucle de relance CAS comme 4x pire que l'arbitrage matériel lock add Reflète probablement encore un cas de conflit maximal . Mais dans les vrais programmes multi-threads, la synchronisation est réduite au minimum (parce que c'est cher), donc les conflits sont faibles et une boucle CAS-retry réussit généralement sans avoir à réessayer.

C++ 11 std::atomicfetch_add Se compilera en un lock add (Ou lock xadd Si la valeur de retour est utilisée), mais un algorithme utilisant CAS pour faire quelque chose cela ne peut pas être fait avec une instruction lock ed n'est généralement pas un désastre. Utilisez C++ 11 std::atomic ou C11 stdatomic au lieu de l'héritage gcc __sync Intégré ou le plus récent __atomic intégré sauf si vous voulez mélanger l'accès atomique et non atomique au même emplacement ...

8.1 DWCAS (cmpxchg16b) : Vous pouvez amener gcc à l'émettre, mais si vous voulez des charges efficaces de seulement la moitié de l'objet , vous avez besoin de vilains hacks union: Comment puis-je implémenter un compteur ABA avec c ++ 11 CAS? . (Ne confondez pas DWCAS avec DCAS de 2 séparé emplacements de mémoire . L'émulation atomique sans verrouillage de DCAS n'est pas possible avec DWCAS, mais la mémoire transactionnelle (comme x86 TSX) le permet.)

8.2.4 mémoire transactionnelle : après quelques faux démarrages (libérés puis désactivés par une mise à jour du microcode en raison d'un bug rarement déclenché), Intel a un fonctionnement transactionnel mémoire dans Broadwell tardif et tous les CPU Skylake. Le design est toujours ce que David Kanter a décrit pour Haswell . Il existe un moyen de verrouillage-ellision pour l'utiliser pour accélérer le code qui utilise (et peut revenir à) un verrou normal (en particulier avec un seul verrou pour tous les éléments d'un conteneur, de sorte que plusieurs threads dans la même section critique ne se heurtent souvent pas ), ou pour écrire directement un code qui connaît les transactions.


7.5 Hugepages : les pages géantes transparentes anonymes fonctionnent bien sous Linux sans avoir à utiliser manuellement hugetlbfs. Faites des allocations> = 2 Mo avec un alignement de 2 Mo (par exemple posix_memalign Ou un aligned_alloc qui n'applique pas la stupide exigence ISO C++ 17 pour échouer lorsque size % alignment != 0).

Une allocation anonyme alignée sur 2 Mo utilisera par défaut des pages géantes. Certaines charges de travail (par exemple, qui continuent à utiliser des allocations importantes pendant un certain temps après les avoir faites) peuvent bénéficier de
echo always >/sys/kernel/mm/transparent_hugepage/defrag pour amener le noyau à défragmenter la mémoire physique chaque fois que nécessaire, au lieu de retomber à 4k pages. (Voir les documents du noya ). Sinon, utilisez madvise(MADV_HUGEPAGE) après avoir effectué de grandes allocations (de préférence toujours avec un alignement de 2 Mo).


Annexe B: Oprofile : Linux perf a principalement remplacé oprofile. Pour les événements détaillés spécifiques à certaines microarchitectures, tilisez le wrapper ocperf.py . par exemple.

ocperf.py stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,\
branches,branch-misses,instructions,uops_issued.any,\
uops_executed.thread,idq_uops_not_delivered.core -r2 ./a.out

Pour quelques exemples d'utilisation, voir Le MOV de x86 peut-il vraiment être "gratuit"? Pourquoi ne puis-je pas reproduire cela du tout? .

93
Peter Cordes

De mon rapide coup d'œil, il semble assez précis. La seule chose à noter, c'est la partie sur la différence entre les contrôleurs de mémoire "intégrés" et "externes". Depuis la sortie de la gamme i7, les processeurs Intel sont tous intégrés, et AMD utilise des contrôleurs de mémoire intégrés depuis la sortie des puces AMD64.

Depuis que cet article a été écrit, peu de choses ont changé, les vitesses sont devenues plus élevées, les contrôleurs de mémoire sont devenus beaucoup plus intelligents (le i7 retardera les écritures à RAM jusqu'à ce qu'il ait envie de commettre les modifications) ), mais pas grand-chose n'a changé. Du moins pas du tout de la manière dont un développeur de logiciels s'en soucierait.

70
Timothy Baldridge