Je me demande dans quelle mesure ce que chaque programmeur devrait savoir sur la mémoire de 2007 d'Ulrich Drepper est toujours valable. De plus, je n'ai pas pu trouver une version plus récente que 1.0 ou un errata.
Je me demande dans quelle mesure ce que chaque programmeur devrait savoir sur la mémoire de 2007 d'Ulrich Drepper est toujours valable. De plus, je n'ai pas pu trouver une version plus récente que 1.0 ou un errata.
Réponses:
Autant que je me souvienne, le contenu de Drepper décrit des concepts fondamentaux sur la mémoire: comment fonctionne le cache du processeur, 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 qui décrit quelque chose de fondamental ne peut pas être qualifié de dépassé. "Ce que tout programmeur devrait savoir sur la mémoire" vaut vraiment la peine d'être lu, mais, eh bien, je ne pense pas que ce soit pour "chaque programmeur". Il est plus adapté aux types système / embarqué / noyau.
Le guide au format PDF est disponible sur 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 réglage 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, réexécuter les benchmarks). Voir aussi d'autres liens d'optimisation des performances x86 et SSE / asm (et C / C ++) dans lex86 tag wiki . (L'article d'Ulrich n'est pas spécifique à x86, mais la plupart (tous) de 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 que celles décrites pour DDR1 / DDR2 (lecture / écriture en rafale). Les améliorations DDR3 / 4 ne sont pas des changements fondamentaux. AFAIK, toutes les choses indépendantes de l'archive s'appliquent toujours en général, par exemple à AArch64 / ARM32.
Consultez également la section Plates-formes liées à la latence de cette réponse pour obtenir des détails importants sur l'effet de la mémoire / latence L3 sur la bande passante à un seul thread:, bandwidth <= max_concurrency / latency
et c'est en fait le principal goulot d'étranglement pour la bande passante à un seul thread sur un processeur moderne à plusieurs cœurs comme un Xeon . Mais un bureau Skylake quadricœur peut être proche de maximiser 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 mémoire monothread? est un résumé.
Ainsi, la suggestion d'Ulrich dans 6.5.8 Utiliser toute la bande passante sur l'utilisation de la mémoire distante sur d'autres nœuds NUMA ainsi que sur le vôtre, est contre-productive sur le matériel moderne où les contrôleurs de mémoire ont plus de bande passante qu'un seul cœur ne peut en utiliser. 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-thread à faible latence, mais en les faisant utiliser la mémoire distante pour des éléments à bande passante élevée non sensibles à la latence. Mais c'est assez obscur, normalement divisez simplement les threads entre les nœuds NUMA et faites-leur 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 même socket peuvent généralement plus que saturer les contrôleurs de mémoire de ce socket.
Une chose majeure qui a changé est que la prélecture matérielle est bien meilleure que sur le Pentium 4 et peut reconnaître les modèles d'accès strided jusqu'à un pas assez large, et plusieurs flux à la fois (par exemple, un avant / arrière par page 4k). Le manuel d'optimisation d'Intel décrit certains détails des pré-chargeurs 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 a des éléments similaires dans son manuel d'optimisation. Attention, le manuel d'Intel regorge également de vieux conseils, dont certains ne sont bons que pour le P4. Les sections spécifiques à Sandybridge sont bien sûr précises pour SnB, mais par exemplele non-laminage des uops micro-fondus a changé dans HSW et le manuel ne le mentionne pas .
Le conseil habituel de nos jours est de supprimer toute la prélecture SW de l'ancien code et d'envisager de la remettre uniquement si le profilage montre que le cache manque (et que vous ne saturez pas la bande passante mémoire). La pré-extraction des deux côtés de l' étape suivante d'une recherche binaire peut toujours aider. par exemple, une fois que vous décidez quel élément regarder ensuite, pré-extraire les éléments 1/4 et 3/4 afin qu'ils puissent se charger en parallèle avec le milieu de chargement / contrôle.
La suggestion d'utiliser un thread de prélecture séparé (6.3.4) est totalement obsolète , je pense, et n'était toujours bonne que sur Pentium 4. P4 avait un hyperthreading (2 cœurs logiques partageant un cœur physique), mais pas assez de trace-cache (et / ou ressources d'exécution dans le désordre) 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 robustes et devraient soit exécuter un vrai thread, soit ne pas utiliser d'hyperthreading (laisser l'autre cœur logique inactif pour que le thread solo ait toutes les ressources au lieu de partitionner le ROB).
La prélecture logicielle 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 charge de 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? . La prélecture NT est intéressante, 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 la dernière goutte de performances et que vous pouvez régler une machine spécifique, la prélecture SW vaut la peine d'être examinée pour un accès séquentiel, mais ellepeut encore ê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 L1D est très élevée et les processeurs modernes peuvent effectuer 2 charges vectorielles par horloge + 1 stockage vectoriel si tout arrive dans L1D. Voir Comment le cache peut-il être aussi rapide?. ) Avec AVX512, taille de la ligne = largeur du vecteur, vous pouvez donc charger / stocker une ligne de cache entière dans une seule instruction. Ainsi, chaque chargement / stockage mal aligné franchit une limite de ligne de cache, au lieu de tous les autres pour 256b AVX1 / AVX2, ce qui souvent ne ralentit pas la boucle 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 fractionnements de page font toujours mal (beaucoup moins sur Skylake, cependant; seulement ~ 11 cycles supplémentaires de latence contre 100, mais toujours une pénalité de débit).
Comme l'avait prédit Ulrich, chaque système multi-socket est de nos jours NUMA: 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 multicœurs sont répandus. Les processeurs Intel de Nehalem à Skylake ont utilisé un grand cache L3 inclus comme un 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 inclusif, 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 fouilles à tous les cœurs. SKX utilise un maillage plutôt qu'un bus en anneau , 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 échecs de cache ou les conflits varient.
6.4.2 Opérations atomiques : le benchmark montrant une boucle de relance CAS comme 4x pire que l'arbitrage matériel lock add
reflète probablement encore un cas de contention maximum . Mais dans de vrais programmes multi-threads, la synchronisation est réduite au minimum (car elle est coûteuse), donc la contention est faible et une boucle de réessai CAS réussit généralement sans avoir à réessayer.
C ++ 11 std::atomic
fetch_add
compilera en a lock add
(ou lock xadd
si la valeur de retour est utilisée), mais un algorithme utilisant CAS pour faire quelque chose qui ne peut pas être fait avec une lock
instruction ed n'est généralement pas un désastre. Utilisez C ++ 11std::atomic
ou C11 au stdatomic
lieu de l' héritage de gcc __sync
ins built- ou les nouveaux __atomic
-ins construits à moins que vous voulez mélanger l' accès atomique et non atomique au même endroit ...
8.1 DWCAS ( cmpxchg16b
) : Vous pouvez convaincre gcc de l'émettre, mais si vous voulez des charges efficaces de seulement la moitié de l'objet, vous avez besoin de union
hacks horribles : Comment puis-je implémenter le compteur ABA avec C ++ 11 CAS? . (Ne confondez pas DWCAS avec DCAS de 2 emplacements de mémoire séparés . L'émulation atomique sans verrouillage de DCAS n'est pas possible avec DWCAS, mais la mémoire transactionnelle (comme x86 TSX) le rend possible.)
8.2.4 Mémoire transactionnelle : après quelques faux départs (libérés puis désactivés par une mise à jour du microcode en raison d'un bogue rarement déclenché), Intel dispose d'une mémoire transactionnelle fonctionnelle dans les derniers modèles Broadwell et tous les processeurs Skylake. Le design est toujours ce que David Kanter a décrit pour Haswell . Il existe une manière de l'utiliser pour accélérer le code qui utilise (et peut revenir vers) 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 du code qui connaît directement les transactions.
7.5 Hugepages : les énormespages anonymes transparentes fonctionnent bien sous Linux sans avoir à utiliser manuellement hugetlbfs. Faites des allocations> = 2MiB avec un alignement de 2MiB (par exemple posix_memalign
, ou unaligned_alloc
qui n'impose pas la stupide exigence ISO C ++ 17 d'échouer quand size % alignment != 0
).
Une allocation anonyme alignée sur 2 Mo utilisera d'énormes pages par défaut. Certaines charges de travail (par exemple, qui continuent à utiliser de grandes allocations pendant un certain temps après les avoir créées) peuvent en bénéficier
echo always >/sys/kernel/mm/transparent_hugepage/defrag
pour que le noyau défragmente la mémoire physique chaque fois que nécessaire, au lieu de retomber à 4k pages. (Voir la documentation du noyau ). Vous pouvez également l'utiliser madvise(MADV_HUGEPAGE)
après avoir fait de grandes allocations (de préférence toujours avec un alignement de 2 Mo).
Annexe B: Oprofile : Linux perf
a pour la plupart remplacé oprofile
. Pour des événements détaillés spécifiques à certaines microarchitectures, utilisez le ocperf.py
wrapper . 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 obtenir des exemples d'utilisation, voir Le MOV de x86 peut-il vraiment être "gratuit"? Pourquoi ne puis-je pas du tout reproduire cela? .
D'après 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 la rédaction de cet article, peu de choses ont changé, les vitesses ont augmenté, les contrôleurs de mémoire sont devenus beaucoup plus intelligents (le i7 retardera les écritures dans la RAM jusqu'à ce qu'il ait envie de commettre les changements), mais pas grand chose n'a changé . Du moins pas du tout dont un développeur de logiciel se soucierait.