Considérez le programme informatique très simple suivant:
for i = 1 to n:
y[i] = x[p[i]]
Ici, et sont des tableaux d'octets à éléments, et est un tableau de mots à éléments. Ici, est grand, par exemple, (de sorte que seule une fraction négligeable des données tient dans n'importe quel type de mémoire cache).
Supposons que constitué de nombres aléatoires , uniformément répartis entre et .
Du point de vue du matériel moderne, cela devrait signifier ce qui suit:
- la lecture de est bon marché (lecture séquentielle)
- la lecture de est très coûteuse (lectures aléatoires; presque toutes les lectures sont des échecs de cache; nous devrons récupérer chaque octet individuel de la mémoire principale)
- écrire est bon marché (écriture séquentielle).
Et c'est bien ce que j'observe. Le programme est très lent par rapport à un programme qui ne fait que des lectures et des écritures séquentielles. Génial.
Vient maintenant la question: dans quelle mesure ce programme se parallélise-t-il sur les plates-formes multicœurs modernes?
Mon hypothèse était que ce programme ne se parallélise pas bien. Après tout, le goulot d'étranglement est la mémoire principale. Un seul cœur perd déjà la plupart de son temps à attendre des données de la mémoire principale.
Cependant, ce n'est pas ce que j'ai observé lorsque j'ai commencé à expérimenter certains algorithmes où le goulot d'étranglement était ce genre d'opération!
J'ai simplement remplacé la for-loop naïve par une for-loop parallèle OpenMP (essentiellement, elle divisera simplement la plage en parties plus petites et exécutera ces parties sur différents cœurs de CPU en parallèle).
Sur les ordinateurs bas de gamme, les accélérations étaient en effet mineures. Mais sur les plates-formes haut de gamme, j'ai été surpris d'obtenir d'excellentes accélérations quasi linéaires. Quelques exemples concrets (les horaires exacts peuvent être un peu décalés, il y a beaucoup de variations aléatoires; ce ne sont que des expériences rapides):
2 x 4 cœurs Xeon (au total 8 cœurs): accélérations de facteur 5 à 8 par rapport à la version à filetage unique.
2 x Xeon à 6 cœurs (au total 12 cœurs): accélérations de facteur 8-14 par rapport à la version à filetage unique.
Maintenant, c'était totalement inattendu. Des questions:
Justement, pourquoi ce type de programme se parallèle-t-il si bien ? Que se passe-t-il dans le matériel? (Ma supposition actuelle est quelque chose dans ce sens: les lectures aléatoires à partir de différents threads sont "pipelinées" et le taux moyen d'obtenir des réponses à celles-ci est beaucoup plus élevé que dans le cas d'un seul thread.)
Est-il nécessaire d'utiliser plusieurs threads et plusieurs cœurs pour obtenir des accélérations? Si une sorte de pipelining a effectivement lieu dans l'interface entre la mémoire principale et le CPU, une application monothread ne pourrait-elle pas faire savoir à la mémoire principale qu'elle aura bientôt besoin de , x [ p [ i + 1 ] ] , ... et l'ordinateur pourrait commencer à récupérer les lignes de cache pertinentes de la mémoire principale? Si cela est possible en principe, comment puis-je y parvenir dans la pratique?
Quel est le bon modèle théorique que nous pourrions utiliser pour analyser ce type de programmes (et faire des prédictions correctes de la performance)?
Edit: Il y a maintenant du code source et des résultats de benchmark disponibles ici: https://github.com/suomela/parallel-random-read
Quelques exemples de chiffres approximatifs ( ):
- environ. 42 ns par itération (lecture aléatoire) avec un seul thread
- environ. 5 ns par itération (lecture aléatoire) avec 12 cœurs.
la source
J'ai décidé d'essayer __builtin_prefetch () moi-même. Je le poste ici comme réponse au cas où d'autres voudraient le tester sur leurs machines. Les résultats sont proches de ce que Jukka décrit: une diminution d'environ 20% du temps d'exécution lors de la pré-extraction de 20 éléments à l'avance par rapport à la pré-extraction de 0 éléments à l'avance.
Résultats:
Code:
la source
L'accès DDR3 est en effet canalisé. http://www.eng.utah.edu/~cs7810/pres/dram-cs7810-protocolx2.pdf les diapositives 20 et 24 montrent ce qui se passe dans le bus mémoire pendant les opérations de lecture en pipeline.
(partiellement faux, voir ci-dessous) Plusieurs threads ne sont pas nécessaires si l'architecture du processeur prend en charge la prélecture du cache. Les x86 et ARM modernes ainsi que de nombreuses autres architectures ont une instruction de prélecture explicite. Beaucoup tentent également de détecter des modèles dans les accès à la mémoire et effectuent la prélecture automatiquement. Le support logiciel est spécifique au compilateur, par exemple GCC et Clang ont intrinsèquement __builtin_prefech () pour la prélecture explicite.
L'hyperthreading de type Intel semble très bien fonctionner pour les programmes qui passent la plupart de leur temps à attendre des échecs de cache. D'après mon expérience, dans une charge de travail intensive en calcul, l'accélération va très peu au-dessus du nombre de cœurs physiques.
EDIT: je me suis trompé au point 2. Il semble que si la prélecture peut optimiser l'accès à la mémoire pour un seul cœur, la bande passante mémoire combinée de plusieurs cœurs est supérieure à la bande passante du cœur unique. Combien plus grand, dépend du CPU.
Le préfetcher matériel et les autres optimisations rendent le benchmarking très délicat. Il est possible de construire des cas où la prélecture explicite a un effet très visible ou inexistant sur les performances, ce benchmark étant l'un de ces derniers.
la source