Ce Que Chaque Programmeur Devrait Savoir Sur La Mémoire?

Je me demande combien de D'Ulrich Drepper ce que chaque programmeur devrait savoir sur la mémoire de 2007 est toujours valide. Aussi, je n'ai pas pu trouver une version plus récente que 1.0 ou un errata.

113
demandé sur Peter Cordes 2011-11-14 22:30:09

3 réponses

Pour autant que je me souvienne, le contenu de Drepper décrit des concepts fondamentaux sur la mémoire: Comment fonctionne le cache du processeur, quelles sont la mémoire physique et virtuelle et comment le noyau Linux traite ce zoo. Il existe probablement des références API obsolètes dans certains exemples, mais cela n'a pas d'importance; cela n'affectera pas la pertinence des concepts fondamentaux.

Donc, tout livre ou article qui décrit quelque chose de fondamental ne peut pas être appelé obsolète. "Ce que chaque programmeur devrait savoir sur la mémoire" vaut vraiment la peine à lire, mais, eh bien, je ne pense pas que ce soit pour "tous les programmeurs". Il est plus approprié pour les gars système/embarqué/noyau.

78
répondu Dan Kruchinin 2014-09-24 17:22:06

De mon coup d'œil rapide-à travers il semble assez précis. La seule chose à remarquer, est la partie sur la différence entre les contrôleurs de mémoire "intégrés" et "externes". Depuis la sortie de la ligne 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, pas beaucoup 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 dans la RAM jusqu'à ce qu'il ait envie de commettre les changements), mais pas beaucoup de choses ont changé. Au moins pas d'aucune façon qu'un développeur de logiciels se soucierait.

65
répondu Timothy Baldridge 2011-11-14 18:40:52

Le guide en format PDF est à 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 quelqu'un 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 x86 performance-tuning et SSE/asm (et C/C++) dans le x86 tag wiki . (L'article d'Ulrich n'est pas spécifique à x86, mais la plupart 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 {[24] } que celles décrites pour DDR1/DDR2 (lecture / écriture en rafale). Les améliorations DDR3 / 4 ne sont pas des changements fondamentaux. AFAIK, tous les trucs indépendants de l'arche s'appliquent toujours généralement, par exemple à AArch64 / ARM32.

Voir aussi la section plates-formes liées à la latence de cette réponse pour des détails importants sur l'effet de la latence de la mémoire/L3 sur la bande passante mono - thread: bandwidth <= max_concurrency / latency, et c'est en fait le goulot d'étranglement principal pour la bande passante mono-thread sur un processeur moderne à plusieurs cœurs comme un Xeon. (Mais un bureau Skylake quad-core peut se rapprocher de maximiser la bande passante DRAM avec un seul thread). Ce lien a de très bonnes informations sur les magasins NT par rapport aux magasins normaux sur x86.

Ainsi, la suggestion D'Ulrich dans 6.5.8 utilisant toute la bande passante (en utilisant la mémoire à distance sur d'autres nœuds NUMA ainsi que le vôtre) est contre-productif sur le 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 certain avantage à 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 de la mémoire distante pour une bande passante élevée. Mais c'est assez obscur; généralement au lieu de en utilisant intentionnellement la mémoire distante lorsque vous auriez pu utiliser local, divisez simplement les threads entre les nœuds NUMA et demandez-leur d'utiliser la mémoire locale.


(habituellement) N'utilisez pas le logiciel prefetch

Une chose importante qui a changé est que le prefetch matériel est beaucoup meilleur que sur P4 et peut reconnaître les modèles d'accès strided jusqu'à une foulée assez grande, et plusieurs flux à la fois (par exemple un avant / arrière par page 4k). manuel d'optimisation D'Intel décrit quelques détails des prefetchers HW dans différents niveaux de cache pour leur microarchitecture de la famille Sandybridge. Ivybridge et plus tard ont prefetch matériel page suivante, au lieu d'attendre un manque de cache dans la nouvelle page pour déclencher un démarrage rapide. (Je suppose QU'AMD a des choses similaires dans leur manuel d'optimisation.) Méfiez-vous que le manuel D'Intel est également plein de vieux conseils, dont certains ne sont bons que pour P4. Les sections spécifiques à Sandybridge sont bien sûr précises pour la BNS, mais par exemple un-lamination des uops micro-fusionnés changé dans HSW et le manuel ne le mentionne pas .

Le conseil habituel de nos jours est de supprimer tous les préréglages SW de l'ancien code, et de ne le remettre en place que si le profilage montre des erreurs de cache (et que vous ne saturez pas la bande passante mémoire). La pré-recherche 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échargez les éléments 1/4 et 3/4 afin qu'ils puissent charger en parallèle avec le chargement / vérification du milieu.

La suggestion d'utiliser un thread prefetch séparé (6.3.4) est totalement obsolète , je pense, et n'était jamais bonne que sur Pentium 4. P4 avait hyperthreading (2 cœurs logiques partageant un noyau physique), mais pas assez de ressources d'exécution ou de trace-cache pour gagner du débit en exécutant deux threads de calcul complets sur le même noyau. Mais les processeurs modernes (Sandybridge-family et Ryzen) sont beaucoup plus costaud et devraient soit exécutez un vrai thread ou n'utilisez pas hyperthreading (laissez l'autre noyau logique inactif afin que le thread solo ait toutes les ressources.)

Software prefetch 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 il ne l'aide pas. cet article de blog montre code + graphiques pour une expérience intéressante dans l'utilisation de SW prefetch sur Haswell pour précharger la partie non séquentielle d'un problème. Voir aussi comment utiliser correctement les instructions prefetch?. 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 goutte de performance, et Vous pouvez syntoniser pour une machine spécifique, SW prefetch vaut la peine d'être regardé pour un accès séquentiel, mais si peut {[56] } être encore un ralentissement si vous avez assez de travail ALU à faire tout en approchant du goulot d'étranglement sur la mémoire.


La Taille de la ligne de Cache est toujours de 64 octets. (La bande passante en lecture / écriture L1D est très élevée, et les processeurs modernes peuvent faire 2 charges vectorielles par horloge + 1 magasin vectoriel si tout frappe dans L1D. voir comment le cache peut-il être aussi rapide?.) Avec AVX512, taille de ligne = largeur de vecteur, de sorte que vous pouvez charger / stocker une ligne de cache entière dans une instruction. (Et donc chaque charge/magasin mal aligné traverse une limite de ligne de cache, au lieu de tous les autres pour 256b AVX1 / AVX2, qui souvent ne ralentit pas la boucle sur un tableau qui n'était pas dans L1D.)

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


Comme Ulrich prédit, chaque système multi-socket est NUMA ces jours-ci: 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-core sont répandus. (Les processeurs Intel de Nehalem à Skylake ont utilisé un grand cache inclusif L3 comme filet de sécurité pour la cohérence entre les cœurs.) 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 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 diffuser des snoops à 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, juste les détails de ce qui se passe exactement quand vous ne pouvez pas éviter les échecs ou les conflits de cache varier.


6.4.2 Atomic ops : Le benchmark montrant une boucle CAS-retry comme 4X pire que lock add arbitré par le matériel reflète probablement encore un cas contention maximale . Mais dans les programmes multithreads réels, la synchronisation est réduite au minimum (car elle est coûteuse), donc la contention est faible et une boucle CAS-retry réussit généralement sans avoir à réessayer.

C++11 std::atomic fetch_add compiler un lock add (ou lock xadd si la valeur de retour est utilisé), mais un l'algorithme utilisant CAS pour faire quelque chose qui ne peut pas être fait avec une instruction locked n'est généralement pas un désastre. Utiliser C++11 std::atomic ou C11 stdatomic au lieu de gcc héritage __sync built-ins, ou la plus récente __atomic built-ins, sauf si vous voulez mélanger atomique et de la non-atomique accès à la même emplacement...

8.1 DCAS (cmpxchg16b): Vous pouvez coaxial gcc en émettant, mais si vous voulez efficace des charges de seulement la moitié de l'objet, vous avez besoin de laide union hacks: Comment puis-je implémenter ABA counter avec C++11 CAS?

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 à cause d'un bug rarement déclenché), Intel dispose de mémoire transactionnelle fonctionnelle dans Broadwell et tous les processeurs Skylake. La conception est toujours ce que David Kanter a décrit pour Haswell . Il existe un moyen de verrouillage pour l'utiliser pour accélérer le code qui utilise (et peut revenir à) un verrou (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 hugepages transparents anonymes fonctionnent bien sous Linux sans avoir à utiliser manuellement hugetlbfs. Faire des allocations > = 2MiB avec un alignement de 2MiB (par exemple posix_memalign, ou un aligned_alloc cela n'impose pas la stupide exigence ISO C++17 d'échouer quand size % alignment != 0).

Une allocation anonyme alignée sur 2mib utilisera hugepages par défaut. Certaines charges de travail (par exemple, qui continuent à utiliser des allocations importantes pendant un certain temps après les avoir créées) peuvent bénéficier de
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 sur des pages 4k. (Voir les documents du noyau ). Alternativement, utilisez madvise(MADV_HUGEPAGE) Après avoir effectué de grandes allocations (de préférence toujours avec un alignement de 2MiB).


Appendice B: Oprofile : Linux perf a principalement remplacé oprofile. Pour les événements détaillés spécifiques à certaines microarchitectures, utilisez 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 de l'utiliser, voir Le MOV De x86 peut-il vraiment être "gratuit"? Pourquoi je ne peux pas reproduire ça du tout?.

41
répondu Peter Cordes 2018-03-25 15:15:30