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.