perf mem
Si vous avez traîné dans les cercles d’optimisation de performances logicielles, vous avez peut-être déjà croisé la phrase “cache rules everything around me”.
En-dehors d’une référence musicale, c’est aussi une réflexion sur le fait que plus le temps passe, plus l’écart de performances entre unités de calcul et bus mémoire se creuse. Ce qui veut dire que pour utiliser efficacement les premières, il faut de moins en moins utiliser le second, et donc tirer de mieux en mieux parti des mémoires cache.
Nous avons déjà vu quelques outils pour analyser l’utilisation qu’on en fait
des caches “à gros grain” (les événements cache et leur suivi via perf stat
et
perf record
). Mais quand on a vraiment envie d’aller dans le détail, ou face à
un code particulièrement complexe, perf mem
est plus puissant.
Là où les formes de perf stat
et perf record
que nous avons abordées se
concentrent sur la fréquence des événements liés au cache et leur localisation
dans le code, perf mem
est focalisé sur les addresses mémoire auxquelles le
code accède, et permet d’établir plus directement quelles données de
l’application doivent en priorité être optimisées pour la localité de cache.
Utilisation de base
Commençons doucement avec notre exemple scale.cpp
, dont la logique d’accès
mémoire est apparente dans le code, ce qui permet de suivre plus facilement ce
que fait perf mem
. Pour la même raison, nous allons le lancer en mode
séquentiel, sans hyperthreading.
cd ~/tp-perf \
&& srun make -j$(nproc) \
&& srun --pty --hint=nomultithread \
perf mem record --ldlat=0 \
./scale.bin 64 1000000000 \
&& perf mem report
perf mem report
affiche alors un menu qui nous permet d’afficher un profil
des lectures…
…et des écritures :
Comment interpréter ces tables ?
- La colonne “Overhead” indique quel pourcentage des échantillons mesurés par
perf
contenaient des accès mémoire avec les propriétés détaillées ensuite, avec pondération par la colonne “Local weight”. - La colonne “Samples” affiche la donnée brute de nombre d’échantillons. Elle n’apporte donc pas de nouvelle information par rapport à “Overhead” mais permet juste de s’assurer rapidement qu’on a une statistique suffisante.
- La colonne “Local weight” attribue un coût relatif à l’accès mémoire, fonction
entre autres choses de la latence d’accès à la donnée pour les lectures.
- Notez que sur les processeurs Intel, on mesure les latences d’utilisation (nombre de cycles écoulés entre le moment où une donnée est demandée par le CPU et le moment où elle est utilisée) et pas seulement les latences de la hiérarchie mémoire (nombre de cycles requis pour rendre une donnée accessible par le CPU). C’est la raison pour laquelle on peut observer des poids très variables sur des codes moins simples.
- La colonne “Memory Access” décrit le type d’accès mémoire (RAM, caches…)
- Les colonnes “Symbol” et “Shared Object” détaillent quelle partie du code a déclenché l’accès.
- Les colonnes “Data Symbol” et “Data Object” l’addresse mémoire ciblée par l’accès.
- La colonne “Snoop” indique la présence de snooping (interception de données émise sur un bus par un autre périphérique connecté).
- La colonne “TLB access” indique si une recherche d’addresse physique a été nécessaire ou si la correspondance virtuelle->physique était en cache (TLB).
- La colonne “Locked” indique si il s’agissait d’accès atomiques Read-Modify-Write. Plus coûteux, ils sont utilisés pour la synchronisation entre threads.
- Je n’ai pas trouvé de documentation sur les dernières colonnes, mes
hypothèses actuelles :
- “Blocked” désigne peut-être les accès invalides qui déclenchent un appel au système d’exploitation (ex : défauts de page).
- “Local INSTR Latency” désigne peut-être à quel point l’exécution de code a été ralentie par cet accès mémoire (dans les cas simples, le CPU peut simplement exécuter d’autres instructions en attendant que les données arrivent).
Dans le cas présent, vous noterez qu’on observe les 8 lectures sur le tas attendus pour un accès à 64 flottants 32-bit en vectorisation AVX (8 flottants par accès), mais aussi deux lectures de la pile du programme inattendus. Ceux-ci sont un effet secondaire des barrières d’optimisation utilisées pour empêcher le compilateur de supprimer tout ou partie des boucles de calcul.
L’inhomogénéité des échantillons pour les accès en écriture est plus surprenante, elle est sans doute liée à des optimisations internes du CPU :
- Deux écritures ciblant une même ligne de cache (bloc aligné de 64 octets sous x86) sont regroupés en une même transaction mémoire à partir du cache L2.
- Une série d’écritures ciblant des addresses mémoire consécutives est un motif d’accès classique, qui est traité de façon optimisée par de nombreux CPUs. Les premiers accès sont alors plus lents que les suivants, le temps que l’optimisation se mette en place.
Exercice : Le paramètre --ldlat=N
de perf mem record
, que j’ai ici forcé
à 0, filtre les données pour ne conserver que les lectures mémoire ayant une
latence supérieure ou égale à N cycles.
En utilisant cette option pour se restreindre à des échantillons intéressants,
augmenter graduellement la taille de boucle interne (premier paramètre de
scale.bin
) tout en diminuant le nombre d’itérations externes (2e paramètre)
pour maintenir le temps d’exécution à peut près constant, et observez le
franchissement des “paliers de cache”.
Vous constaterez rapidement que l’utilisation de perf mem
sur des programmes
non triviaux nécessite la mise en place de logs permettant de suivre
l’emplacement en mémoire des différentes allocations tas effectuées par le
programme, avant de pouvoir interpréter correctement les addresses mémoire
présentes dans la sortie de perf help report
! Sur des programmes bien
optimisés qui font peu d’allocations, cela peut être fait en utilisant un
allocateur mémoire spécial qui affiche la pile d’appel de l’appelant à chaque
allocation.
Utilisation avancée
Les commandes perf mem
sont en réalité un raccourci vers des commandes
perf record
et perf report
un peu complexes.
A l’heure où ces lignes sont écrites, perf mem record --ldlat=<N>
est équivalent
à perf record -e cpu/mem-loads,ldlat=<N>/P -e cpu/mem-stores/P --data --weight
et perf mem report
est à peu près équivalent à
perf report --mem-mode --sort=overhead,sample,local_weight,mem,sym,dso,symbol_daddr,dso_daddr,snoop,tlb,locked,blocked,local_ins_lat
.
L’intérêt de connaître ces commandes brutes est que…
- Côté
record
, cela vous permet d’acquérir d’autres mesures en même temps que le profil mémoire, sur la même exécution du programme. - Côté
report
, cela vous permet d’afficher des profils simplifiés. Par exemple de ne faire que catégoriser les accès mémoire, sans les décomposer pour chaque addresse de données, avec un--sort
n’incluant pas la clésymbol_daddr
. Ou bien d’utiliser d’autres clés de tri.
Exercice : Affichez un profil de scale.bin
ayant pour seules clés de tri
l’overhead, le nombre d’échantillons, le type d’accès mémoire, le symbole où
l’accès mémoire est survenu et le binaire associé, et les propriétés snoop
,
tlb
, locked
et blocked
.
Vous obtiendrez ainsi un résumé de l’activité mémoire du programme un peu plus
détaillé que ce qu’on peut obtenir juste avec perf stat
.
Autres options
Au vu de la parenté de perf mem
avec perf record
et perf report
, il ne
vous surprendra pas que les options de ces dernières commandes soient
supportés par la commande perf mem
associée.
Il y a tout de même quelques options intéressantes spécifiques à perf mem
,
notamment…
- L’option
--type
, qui s’abbrévie en-t
, permet de ne mesurer que des lectures (load
) ou des écritures (store
). - L’option
--dump-raw-samples
, qui s’abbrévie en-D
, permet d’afficher les échantillons dans un format facile à traiter par une machine, à la manière deperf script
. - Les options
--all-user
et--all-kernel
, qui s’abbrévient respectivement en-U
et-K
, jouent un rôle équivalent aux options:u
et:k
dans les noms d’événement : elles permettent de se limiter à l’activité applicative ou à l’activité noyau.
Comme d’habitude, n’hésitez pas à consulter perf help mem
pour plus de détails.