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…

Rapport sur les lectures

…et des écritures :

Rapport sur les é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 :

  1. 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.
  2. 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 de perf 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.