perf c2c
Dans le milieu du parallélisme, il est de notoriété publique que synchroniser des threads a un coût non négligeable, et il est également assez connu qu’un code parallèle qui utilise trop de synchronisation ne sera pas seulement aussi lent qu’un code séquentiel, il sera beaucoup plus lent.
Ce qui est moins connu, c’est que cette lenteur provient surtout de deux mécanismes matériels :
- L’utilisation d’opérations atomiques Read-Modify-Write dans les primitives
de synchronisation, qui sont bien plus coûteuses que des instructions
non-atomiques équivalentes. On a vu que ces opérations atomiques peuvent être
étudiées avec
perf mem
. - La cohérence de cache, qui impose qu’à chaque instant, on ait soit un seul coeur CPU qui a un accès exclusif à une donnée, soit plusieurs coeurs CPU qui y ont accès en lecture seule. Ce qui signifie que quand un thread accède à une donnée écrite par un autre thread, la ligne de cache doit être transférée d’un coeur CPU à un autre. Cette opération a un coût conséquent, d’autant plus élevé que les threads sont sur des coeurs CPUs physiquement éloignés.
La cohérence de cache opère, comme son nom le suggère, avec la granularité d’une ligne de cache, soit 64 octets alignés en mémoire sur les processeurs x86 actuels. Cela signifie que si au sein de ce même bloc de données il y a deux variables utilisées par deux threads différents, même si ces deux threads ne communiquent jamais par le biais de ces variable, on observera une perte importante de performances due au fait que la ligne de cache associée passe son temps à sauter d’un coeur CPU à l’autre à cause de la cohérence de cache.
On parle alors de false sharing, et perf c2c
est un outil conçu pour aider à
étudier ce phénomène.
Introduction
L’exemple pingpong.cpp
met en évidence le phénomène de false sharing dans un
cadre simplifié. Un groupe de N threads partagent un tableau de N octets et
lisent/écrivent chacun à un emplacement dédié du tableau. C’est l’exemple type
de ce qu’il ne faut pas faire, et en dépit de l’absence de synchronisation
après le démarrage du programme l’effet de false sharing sera extrêmement
présent dans cette configuration.
Mesurons donc ce phénomène avec perf c2c
en faisant attention à l’organisation
des données…
cd ~/tp-perf \
&& srun make -j$(nproc) \
&& srun --pty -n1 -c16 --hint=nomultithread \
perf c2c record --all-user \
./pingpong.bin 16 2000000000
Ici, on utilise l’option --all-user
, qui s’abbrévie en -u
, pour ignorer les
conflits au niveau du noyau Linux qui ne nous intéressent pas ici.
Maintenant, affichons les résultats de perf c2c
:
perf c2c report
perf c2c
commence par afficher une table des lignes de cache pour lesquels
plusieurs accès par différents threads ont été détectés. Pour chaque ligne de
cache, sont indiqués…
- Son addresse
- Le noeud NUMA sur lequel elle réside (sur ce système il n’y en a qu’un seul)
- “PA cnt” indique le
nombre de fois où l’addresse physique à laquelle un thread accède a changé entre un échantillon
perf
et le suivant. Ici, les threads passent leur temps à accéder à des offsets différents au sein de la ligne de cache, donc ce nombre est très élevé. - Des statistiques sur le nombre de conflits détectés au niveau du cache L3 ou
de la RAM (HITM = “HIT in a Modified cache line”) :
- Fraction des conflits associés à chaque ligne de cache
- Nombre total de conflits
- Décomposition en conflits locaux (au sein d’un même noeud NUMA) et distant (sur un autre noeud NUMA, plus cher).
- Une distribution des loads et des stores analogue à celle qu’on peut
obtenir avec
perf mem
.
Notez que même dans cette configuration extrême, peu de conflits sont détectés au niveau du cache L3, ce qui est plutôt surprenant. En revanche, il est tout à fait normal qu’aucun accès RAM ne soit détecté, puisque même en situation de false sharing, les données restent dans un cache CPU.
Grâce à l’instrumentation de ping_pong.bin
, on trouve rapidement le tableau
partagé entre les threads (adresse 0x1f15f00) dans cette table. On y voit
qu’environ 5% des lectures échantillonnées par perf
ont dû passer par le Fill
Buffer (interface entre le cache L1 et le cache L2), et que quelques écritures
(0.5%) ne vont pas non plus dans le cache L1, mais dans un niveau de cache inférieur.
Avec la touche d
, on peut obtenir un affichage plus détaillé pour cette ligne
de cache…
…où l’on a le détail des accès observés, triés par offset, région du code concernée et noeud NUMA :
- Les statistiques d’accès sont données en pourcentage des données pour la ligne de cache.
- Pour chaque ligne de cache (“CL” = Cache Line), on a le détail par offset, et ici on a une valeur correcte du nombre d’adresses physiques.
- Puis on voit quel code a accédé aux données (offset brut dans le code source et à droite symbole, binaire et ligne de code source si disponible).
- Au milieu, on a une section “cycles” qui indique le nombre de cycles CPU
cumulés utilisés pour les accès échantillonnés par
perf c2c
. On observe que bien que les accès HITM soient fortement minoritaires dans les mesures, leur contribution est énorme par rapport à celle des lectures normales. - Ensuite on a des données brutes de nombre d’échantillons, pour vérifier la qualité du résultat.
- Puis on a le nombre de coeurs CPU qui ont accédé à chaque offset. Il est
curieux que pour un des offset on ait 2 CPUs ici. Je soupçonne que c’est
soit que
perf
a échantillonné l’initialisation du tableau, soit qu’un thread a migré d’un coeur CPU à un autre. - Enfin, sur la colonne de droite, on aurait, sur un système NUMA, des
information sur le noeud exécutant chaque thread. On peut, avec la touche
n
, basculer dans un mode où les numéros de CPUs concernés sont affichés.
Exercice : Variez le nombre de threads (premier paramètre de l’exemple
pingpong.bin
et observez comment cela affecte le temps d’exécution et les
statistiques de false sharing mesurées par perf c2c
. Vous aurez peut-être
besoin de l’option --show-all
décrite ci-dessous…
Autres options
Par défault, perf c2c report
n’affiche que les lignes de caches pour
lesquelles des accès au cache L3 de type “HITM” ont été détecté. La détection de
tels accès est, on l’a vu, assez rare, donc il arrive fréquemment qu’aucun accès
HITM ne soit détecté, et donc qu’aucune ligne de cache ne soit affichée. Vous
pouvez désactiver ce filtrage avec l’option --show-all
.
perf c2c record
prend des options similaires à perf mem record
, et
perf c2c record --ldlat=<N>
est équivalent à
perf record --weight --data --phys-data --sample-cpu -e cpu/mem-loads,ldlat=<N>/P -e cpu/mem-stores/P
.
On peut également passer des options spécifiques à perf record
après une paire
de tirets --
.
En revanche, contrairement à perf mem report
, perf c2c report
ne peut pas
être imité/reconfiguré avec perf report
.
Parmi les options intéressantes de perf c2c report
, détaillées dans
perf help c2c
, mentionnons la possibilité d’avoir un rapport non interactif
(--stdio
) et celle d’afficher une pile d’appel pour les accès concurrents afin
de mieux comprendre comment on en est arrivé à exécuter chaque code.