dimanche 1 juin 2014

Linux : Numérotation des processeurs et topologie d'iceux

Comptons les processeurs, 

Y'a pas de thread chez nous, 

Y'en a chez la voisine... 

Besoin primaire

Sur des machines disposant de plusieurs processeurs, il arrive que l'on ait besoin pour (tenter d') assurer qu'un processus ait de bonnes performances, que l'on "épingle" le processus sur un processeur: en clair, on indique au système qu'il doit impérativement exécuter le dit processus sur le processeur de numéro N.

Si le processus épinglé a plusieurs threads logicielles, ou si l'on a plusieurs processus, on peut être amené à forcer l'exécution de ces différentes threads (ou processus) sur différents processeurs (numéros N, N+1, N+2...).

Facile donc ?

Posix nous fournit tout ce qu'il faut ? Euh non. Quand on a ce genre de besoins, il faut utiliser les outils spécifiques du système. Portabilité ? Ben, non. De toute manière, si vous faîtes ça pour des raisons d'amélioration de performances, votre travail est à faire (ou pour le moins à adapter) pour chaque système d'accueil.

Fixer un (des) processeur(s) d'exécution sur Linux

  • taskset est une commande qui permet de contraindre l'exécution d'un programme ou d'un processus sur un ou des processeurs spécifiés en paramètre.
  • Par programmation en C, on peut utiliser sched_affinity ou pthread_setaffinity_np.
Cliquer sur les liens ci-dessus pour obtenir les pages  manuels.

 Facile donc

Oui, il suffit d'utiliser les outils ci-dessus et le tour est joué.  :-)
Ça a pourtant l'air facile. On fixe des numéros de processeurs, en vérifiant que l'on nomme des processeurs existants de la machine (pour simplifier : de numéro inférieur au nombre de processeurs de la machine, que l'on peut obtenir sur Linux par la commande nproc).

En fait, et bien évidemment, c'est  souvent,  sinon le début des ennuis, tout au moins l'entrée dans un espace de complexité accrue. Tant pis pour vous. 

Un processeur = un processeur ?

Ce n'est malheureusement pas une question de philo. Ce n'est pas vraiment qu'il y ait des processeurs plus z'égaux que d'autres. Mais il faut quand même être très attentif. Je sais, tu sais, il sait, nous savons,... qu'il existe des processeurs multi-cœurs et des processeurs hyper-threadés. Pour simplifier, ne considérons que le monde x86 (32 et/ou 64 bits).

"Hiérarchie de processeurs"

On a donc des:
  • processeurs (puces avec plein de pattes qui s'enfichent dans des "sockets" sur la carte mère, et que du coup on appelle assez souvent sockets) qui contiennent
    • des cœurs (cores) que l'on peut parfois appeler abusivement processeurs qui eux-mêmes contiennent
      • des hyper-threads que l'on appelle souvent et abusivement des processeurs.
Pas question  ici de rentrer dans le détail des subtilités. Mais de manière très schématique:
  • les hyper-threads d'un même cœur partagent souvent les mêmes unités de calculs et les caches mémoires L1, L2, L3. Comme elles se partagent les unités de calcul, la puissance de calcul de ces deux hyper-threads, n'est (très généralement) pas le double de celle d'un processeur sans hyper-thread. (On reviendra sur cet aspect dans un article ultérieur).
  • Les cœurs d'une même puce peuvent partager des caches L3 et L2.
  • Des puces différentes ne partagent rien. Quoique....
Si on considère des machines un peu plus puissantes on va avoir en plus des machines de type NUMA (Non Uniform Memory Access) qui vont venir nous compliquer la tâche. Les temps d'accès à une adresse mémoire donnée ne sont pas identiques en fonction du processeur (puce) depuis lequel on référence cette adresse mémoire.

Problèmes de choix

Du coup,voilà quelques problèmes :
  • Si on met 2 processus (pthreads) sur deux cpu (hyper-threads) du même cœur, ils vont se partager (ou être en compétition pour) l'utilisation du cache L1. Mais surtout la puissance de calcul n'est pas forcément celle espérée.
  • Si on met 2 processus (pthreads) sur deux cpu sur des cœurs différents, en supposant qu'il n'y ait pas d'autres processus squattant les hyper-threads co-localisées sur ces cœurs, la puissance de calcul "disponible" devrait être meilleure. Mais les partages de caches étant différents, qui sait... 
Il ne s'agit pas ici, malheureusement, de fournir les clés permettant de répondre à ces questions. Déception. Consternation. Mais simplement de se dire que le placement des processus sur les cpu est une chose importante.  Du coup, il devient nécessaire de savoir si deux CPU de numéros respectifs N et M partagent ou ne partagent pas le même cœur, et/ou les mêmes caches.

De la numérotation à la topologie

Naïvement, on pourrait penser que si le cpu N°0 correspond à la première hyper-thread du premier cœur de la première puce, le cpu  N°1 serait logiquement la deuxième hyper-thread du premier cœur de la première puce...

Perdu!

Comment savoir alors ? Grâce à la commande  lscpu (voir la page manuel). Voici ce qu'elle indique sur une machine :

fa$ lscpu
Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                24
On-line CPU(s) list:   0-23
Thread(s) per core:    2
Core(s) per socket:    6
CPU socket(s):         2
NUMA node(s):          2
Vendor ID:             GenuineIntel
CPU family:            6
Model:                 45
Stepping:              7
CPU MHz:               1901.000
BogoMIPS:              3799.47
Virtualization:        VT-x
L1d cache:             32K
L1i cache:             32K
L2 cache:              256K
L3 cache:              15360K
NUMA node0 CPU(s):     0,2,4,6,8,10,12,14,16,18,20,22
NUMA node1 CPU(s):     1,3,5,7,9,11,13,15,17,19,21,23

fa$
Bon , pas très informatif. Ah si notre machine a :
  • 2 sockets (puces)
  • qui sont équipées chacune de 6 cœurs
  •  qui sont chacun "hyper-threadé".
On a donc 24 processeurs à disposition.   Apparemment notre machine a deux nœuds NUMA : l'accès depuis un des processeurs  du nœud 0 (les cpu de numéro pair) à la mémoire  du nœud 0 sera plus rapide qu'un accès à la mémoire du nœud 1 depuis ces mêmes cpu de numéro pair.

En utilisant l'argument -p, on obtient un résultat difficilement lisible mais beaucoup plus instructif:
fa$ lscpu -p
# The following is the parsable format, which can be fed to other
# programs. Each different item in every column has an unique ID
# starting from zero.
# CPU,Core,Socket,Node,,L1d,L1i,L2,L3
0,0,0,0,,0,0,0,0
1,1,1,1,,1,1,1,1
2,2,0,0,,2,2,2,0
3,3,1,1,,3,3,3,1
4,4,0,0,,4,4,4,0
5,5,1,1,,5,5,5,1
6,6,0,0,,6,6,6,0
7,7,1,1,,7,7,7,1
8,8,0,0,,8,8,8,0
9,9,1,1,,9,9,9,1
10,10,0,0,,10,10,10,0
11,11,1,1,,11,11,11,1
12,0,0,0,,0,0,0,0
13,1,1,1,,1,1,1,1
14,2,0,0,,2,2,2,0
15,3,1,1,,3,3,3,1
16,4,0,0,,4,4,4,0
17,5,1,1,,5,5,5,1
18,6,0,0,,6,6,6,0
19,7,1,1,,7,7,7,1
20,8,0,0,,8,8,8,0
21,9,1,1,,9,9,9,1
22,10,0,0,,10,10,10,0
23,11,1,1,,11,11,11,1

fa$

On voit ici que ce sont les cpu N°0 et N°12 qui sont "co-localisés" sur le même cœur de la même socket, et qu'ils partagent les mêmes caches L1, L2, L3...

Voilà!

"Petits compléments"

Si l'envie vous prenait de plonger plus avant dans les problèmes de topologie des processeurs et caches, prenez la peine de lire :

Aucun commentaire:

Enregistrer un commentaire