33 votes

Mesurer NUMA (accès mémoire non uniforme). Pas d'asymétrie observable. Pourquoi?

J'ai essayé de mesurer l'asymétrie de l'accès à la mémoire les effets de NUMA, et a échoué.

L'Expérience

Exécuté sur un processeur Intel Xeon X5570 @ 2.93 GHz, 2 Processeurs 8 cœurs.

Sur un fil épinglé core 0, I allouer un tableau x de taille 10 000 000 octets sur le core 0 nœud NUMA avec numa_alloc_local. Puis-je effectuer une itération sur tableau x 50 fois de lire et d'écrire chaque octet dans le tableau. Mesurer le temps écoulé à faire le 50 itérations.

Ensuite, sur chaque de l'autre des carottes dans mon serveur, j'ai broches un nouveau thread et mesurer à nouveau le temps écoulé 50 itérations de la lecture et de l'écriture pour chaque octet de la matrice x.

Tableau x est grand pour minimiser les effets de cache. Nous voulons mesurer la vitesse du PROCESSEUR est d'aller tout le chemin à la RAM pour charger et stocker, et non pas lorsque les caches sont à l'aider.

Il y a deux nœuds NUMA dans mon serveur, donc je m'attends les cœurs qui ont de l'affinité sur le même nœud dans lequel la matrice x est affecté à avoir plus rapide vitesse de lecture/écriture. Je ne vois pas de qui.

Pourquoi?

Peut-être NUMA n'est pertinente que sur les systèmes avec > de 8 à 12 cœurs, comme je l'ai vu proposé ailleurs?

http://lse.sourceforge.net/numa/faq/

numatest.cpp

#include <numa.h>
#include <iostream>
#include <boost/thread/thread.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <pthread.h>

void pin_to_core(size_t core)
{
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(core, &cpuset);
    pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);
}

std::ostream& operator<<(std::ostream& os, const bitmask& bm)
{
    for(size_t i=0;i<bm.size;++i)
    {
        os << numa_bitmask_isbitset(&bm, i);
    }
    return os;
}

void* thread1(void** x, size_t core, size_t N, size_t M)
{
    pin_to_core(core);

    void* y = numa_alloc_local(N);

    boost::posix_time::ptime t1 = boost::posix_time::microsec_clock::universal_time();

    char c;
    for (size_t i(0);i<M;++i)
        for(size_t j(0);j<N;++j)
        {
            c = ((char*)y)[j];
            ((char*)y)[j] = c;
        }

    boost::posix_time::ptime t2 = boost::posix_time::microsec_clock::universal_time();

    std::cout << "Elapsed read/write by same thread that allocated on core " << core << ": " << (t2 - t1) << std::endl;

    *x = y;
}

void thread2(void* x, size_t core, size_t N, size_t M)
{
    pin_to_core(core);

    boost::posix_time::ptime t1 = boost::posix_time::microsec_clock::universal_time();

    char c;
    for (size_t i(0);i<M;++i)
        for(size_t j(0);j<N;++j)
        {
            c = ((char*)x)[j];
            ((char*)x)[j] = c;
        }

    boost::posix_time::ptime t2 = boost::posix_time::microsec_clock::universal_time();

    std::cout << "Elapsed read/write by thread on core " << core << ": " << (t2 - t1) << std::endl;
}

int main(int argc, const char **argv)
{
    int numcpus = numa_num_task_cpus();
    std::cout << "numa_available() " << numa_available() << std::endl;
    numa_set_localalloc();

    bitmask* bm = numa_bitmask_alloc(numcpus);
    for (int i=0;i<=numa_max_node();++i)
    {
        numa_node_to_cpus(i, bm);
        std::cout << "numa node " << i << " " << *bm << " " << numa_node_size(i, 0) << std::endl;
    }
    numa_bitmask_free(bm);

    void* x;
    size_t N(10000000);
    size_t M(50);

    boost::thread t1(boost::bind(&thread1, &x, 0, N, M));
    t1.join();

    for (size_t i(0);i<numcpus;++i)
    {
        boost::thread t2(boost::bind(&thread2, x, i, N, M));
        t2.join();
    }

    numa_free(x, N);

    return 0;
}

La Sortie

g++ -o numatest -pthread -lboost_thread -lnuma -O0 numatest.cpp

./numatest

numa_available() 0                    <-- NUMA is available on this system
numa node 0 10101010 12884901888      <-- cores 0,2,4,6 are on NUMA node 0, which is about 12 Gb
numa node 1 01010101 12874584064      <-- cores 1,3,5,7 are on NUMA node 1, which is slightly smaller than node 0

Elapsed read/write by same thread that allocated on core 0: 00:00:01.767428
Elapsed read/write by thread on core 0: 00:00:01.760554
Elapsed read/write by thread on core 1: 00:00:01.719686
Elapsed read/write by thread on core 2: 00:00:01.708830
Elapsed read/write by thread on core 3: 00:00:01.691560
Elapsed read/write by thread on core 4: 00:00:01.686912
Elapsed read/write by thread on core 5: 00:00:01.691917
Elapsed read/write by thread on core 6: 00:00:01.686509
Elapsed read/write by thread on core 7: 00:00:01.689928

Faire 50 itérations de la lecture et de l'écriture sur tableau x prend environ 1,7 secondes, n'importe qui de base est de faire de la lecture et de l'écriture.

Mise à jour:

La taille du cache sur mon Cpu est de 8 mo, donc peut-être que de 10 mo matrice x n'est pas assez grand pour éliminer cache effecs. J'ai essayé de 100 mo tableau x, et J'ai essayé la délivrance d'un mémoire complet clôture __synchronisation_synchronize() à l'intérieur de mon plus profond de boucles. Il ne révèle pas de l'asymétrie entre les nœuds NUMA.

Mise à jour 2:

J'ai essayé de la lecture et de l'écriture à la matrice x avec __synchronisation_fetch_et_add(). Toujours rien.

19voto

Mysticial Points 180300

La première chose que je veux souligner est que vous pouvez double-vérifier que les cœurs sont sur chaque nœud. Je ne me souviens pas des carottes et des nœuds entrelacés comme ça. Aussi, vous devriez avoir 16 threads en raison de HT. (sauf si vous l'avez désactivée)

Autre chose:

Le socket 1366 Xeon machines ne sont que légèrement NUMA. De sorte qu'il sera difficile de voir la différence. Le NUMA effet est beaucoup plus perceptible sur les 4P Opterons.

Sur les systèmes comme le vôtre, le nœud à l'autre de la bande passante est effectivement plus rapide que le PROCESSEUR à la mémoire de la bande passante. Depuis votre motif de l'accès est complètement séquentiel, vous obtenez toute la bande passante, indépendamment de si oui ou non les données sont locales. Une meilleure chose pour la mesurer est le temps de latence. Essayez aléatoire de l'accès à un bloc de 1 GO au lieu de streaming de façon séquentielle.

Dernière chose:

En fonction de l'agressivité de votre compilateur optimise votre boucle, pourrait être optimisé car il ne fait rien:

c = ((char*)x)[j];
((char*)x)[j] = c;

Quelque chose comme ceci permettra de garantir qu'il ne sera pas éliminé par le compilateur:

((char*)x)[j] += 1;

15voto

James Brock Points 1502

Ah Ah! Mysticial est à droite! En quelque sorte, de matériel de pré-chargement est l'optimisation de mes lectures/écritures.

Si il s'agissait d'une cache d'optimisation, puis forcer une barrière de mémoire à l'encontre de l'optimisation:

c = __sync_fetch_and_add(((char*)x) + j, 1);

mais cela ne fait aucune différence. Ce qui fait une différence, c'est en multipliant mon itérateur index par le premier 1009 pour vaincre le pré-chargement d'optimisation:

*(((char*)x) + ((j * 1009) % N)) += 1;

Avec ce changement, le NUMA asymétrie est clairement révélé:

numa_available() 0
numa node 0 10101010 12884901888
numa node 1 01010101 12874584064
Elapsed read/write by same thread that allocated on core 0: 00:00:00.961725
Elapsed read/write by thread on core 0: 00:00:00.942300
Elapsed read/write by thread on core 1: 00:00:01.216286
Elapsed read/write by thread on core 2: 00:00:00.909353
Elapsed read/write by thread on core 3: 00:00:01.218935
Elapsed read/write by thread on core 4: 00:00:00.898107
Elapsed read/write by thread on core 5: 00:00:01.211413
Elapsed read/write by thread on core 6: 00:00:00.898021
Elapsed read/write by thread on core 7: 00:00:01.207114

Au moins, je pense que c'est ce qui se passe.

Grâce Mysticial!

EDIT: CONCLUSION ~133%

Pour quelqu'un qui est juste en jetant un coup d'oeil à ce post pour avoir une idée approximative des caractéristiques de performance de NUMA, voici la ligne de fond après mes tests:

L'accès à la mémoire d'un non-local nœud NUMA a environ 1,33 fois le temps de latence d'accès à la mémoire d'un nœud local.

10voto

Andreas Klöckner Points 731

Merci pour ce test de code. J'ai pris votre "fixe" de la version et de le changer en pur C + OpenMP et ajouté quelques tests pour savoir comment le système de mémoire se comporte en vertu de la prétention. Vous pouvez trouver le nouveau code ici.

Voici quelques exemples de résultats à partir d'un Opteron Quad:

num cpus: 32
numa available: 0
numa node 0 10001000100010000000000000000000 - 15.9904 GiB
numa node 1 00000000000000001000100010001000 - 16 GiB
numa node 2 00010001000100010000000000000000 - 16 GiB
numa node 3 00000000000000000001000100010001 - 16 GiB
numa node 4 00100010001000100000000000000000 - 16 GiB
numa node 5 00000000000000000010001000100010 - 16 GiB
numa node 6 01000100010001000000000000000000 - 16 GiB
numa node 7 00000000000000000100010001000100 - 16 GiB

sequential core 0 -> core 0 : BW 4189.87 MB/s
sequential core 1 -> core 0 : BW 2409.1 MB/s
sequential core 2 -> core 0 : BW 2495.61 MB/s
sequential core 3 -> core 0 : BW 2474.62 MB/s
sequential core 4 -> core 0 : BW 4244.45 MB/s
sequential core 5 -> core 0 : BW 2378.34 MB/s
sequential core 6 -> core 0 : BW 2442.93 MB/s
sequential core 7 -> core 0 : BW 2468.61 MB/s
sequential core 8 -> core 0 : BW 4220.48 MB/s
sequential core 9 -> core 0 : BW 2442.88 MB/s
sequential core 10 -> core 0 : BW 2388.11 MB/s
sequential core 11 -> core 0 : BW 2481.87 MB/s
sequential core 12 -> core 0 : BW 4273.42 MB/s
sequential core 13 -> core 0 : BW 2381.28 MB/s
sequential core 14 -> core 0 : BW 2449.87 MB/s
sequential core 15 -> core 0 : BW 2485.48 MB/s
sequential core 16 -> core 0 : BW 2938.08 MB/s
sequential core 17 -> core 0 : BW 2082.12 MB/s
sequential core 18 -> core 0 : BW 2041.84 MB/s
sequential core 19 -> core 0 : BW 2060.47 MB/s
sequential core 20 -> core 0 : BW 2944.13 MB/s
sequential core 21 -> core 0 : BW 2111.06 MB/s
sequential core 22 -> core 0 : BW 2063.37 MB/s
sequential core 23 -> core 0 : BW 2082.75 MB/s
sequential core 24 -> core 0 : BW 2958.05 MB/s
sequential core 25 -> core 0 : BW 2091.85 MB/s
sequential core 26 -> core 0 : BW 2098.73 MB/s
sequential core 27 -> core 0 : BW 2083.7 MB/s
sequential core 28 -> core 0 : BW 2934.43 MB/s
sequential core 29 -> core 0 : BW 2048.68 MB/s
sequential core 30 -> core 0 : BW 2087.6 MB/s
sequential core 31 -> core 0 : BW 2014.68 MB/s

all-contention core 0 -> core 0 : BW 1081.85 MB/s
all-contention core 1 -> core 0 : BW 299.177 MB/s
all-contention core 2 -> core 0 : BW 298.853 MB/s
all-contention core 3 -> core 0 : BW 263.735 MB/s
all-contention core 4 -> core 0 : BW 1081.93 MB/s
all-contention core 5 -> core 0 : BW 299.177 MB/s
all-contention core 6 -> core 0 : BW 299.63 MB/s
all-contention core 7 -> core 0 : BW 263.795 MB/s
all-contention core 8 -> core 0 : BW 1081.98 MB/s
all-contention core 9 -> core 0 : BW 299.177 MB/s
all-contention core 10 -> core 0 : BW 300.149 MB/s
all-contention core 11 -> core 0 : BW 262.905 MB/s
all-contention core 12 -> core 0 : BW 1081.89 MB/s
all-contention core 13 -> core 0 : BW 299.173 MB/s
all-contention core 14 -> core 0 : BW 299.025 MB/s
all-contention core 15 -> core 0 : BW 263.865 MB/s
all-contention core 16 -> core 0 : BW 432.156 MB/s
all-contention core 17 -> core 0 : BW 233.12 MB/s
all-contention core 18 -> core 0 : BW 232.889 MB/s
all-contention core 19 -> core 0 : BW 202.48 MB/s
all-contention core 20 -> core 0 : BW 434.299 MB/s
all-contention core 21 -> core 0 : BW 233.274 MB/s
all-contention core 22 -> core 0 : BW 233.144 MB/s
all-contention core 23 -> core 0 : BW 202.505 MB/s
all-contention core 24 -> core 0 : BW 434.295 MB/s
all-contention core 25 -> core 0 : BW 233.274 MB/s
all-contention core 26 -> core 0 : BW 233.169 MB/s
all-contention core 27 -> core 0 : BW 202.49 MB/s
all-contention core 28 -> core 0 : BW 434.295 MB/s
all-contention core 29 -> core 0 : BW 233.309 MB/s
all-contention core 30 -> core 0 : BW 233.169 MB/s
all-contention core 31 -> core 0 : BW 202.526 MB/s

two-contention core 0 -> core 0 : BW 3306.11 MB/s
two-contention core 1 -> core 0 : BW 2199.7 MB/s

two-contention core 0 -> core 0 : BW 3286.21 MB/s
two-contention core 2 -> core 0 : BW 2220.73 MB/s

two-contention core 0 -> core 0 : BW 3302.24 MB/s
two-contention core 3 -> core 0 : BW 2182.81 MB/s

two-contention core 0 -> core 0 : BW 3605.88 MB/s
two-contention core 4 -> core 0 : BW 3605.88 MB/s

two-contention core 0 -> core 0 : BW 3297.08 MB/s
two-contention core 5 -> core 0 : BW 2217.82 MB/s

two-contention core 0 -> core 0 : BW 3312.69 MB/s
two-contention core 6 -> core 0 : BW 2227.04 MB/s

two-contention core 0 -> core 0 : BW 3287.93 MB/s
two-contention core 7 -> core 0 : BW 2209.48 MB/s

two-contention core 0 -> core 0 : BW 3660.05 MB/s
two-contention core 8 -> core 0 : BW 3660.05 MB/s

two-contention core 0 -> core 0 : BW 3339.63 MB/s
two-contention core 9 -> core 0 : BW 2223.84 MB/s

two-contention core 0 -> core 0 : BW 3303.77 MB/s
two-contention core 10 -> core 0 : BW 2197.99 MB/s

two-contention core 0 -> core 0 : BW 3323.19 MB/s
two-contention core 11 -> core 0 : BW 2196.08 MB/s

two-contention core 0 -> core 0 : BW 3582.23 MB/s
two-contention core 12 -> core 0 : BW 3582.22 MB/s

two-contention core 0 -> core 0 : BW 3324.9 MB/s
two-contention core 13 -> core 0 : BW 2250.74 MB/s

two-contention core 0 -> core 0 : BW 3305.66 MB/s
two-contention core 14 -> core 0 : BW 2209.5 MB/s

two-contention core 0 -> core 0 : BW 3303.52 MB/s
two-contention core 15 -> core 0 : BW 2182.43 MB/s

two-contention core 0 -> core 0 : BW 3352.74 MB/s
two-contention core 16 -> core 0 : BW 2607.73 MB/s

two-contention core 0 -> core 0 : BW 3092.65 MB/s
two-contention core 17 -> core 0 : BW 1911.98 MB/s

two-contention core 0 -> core 0 : BW 3025.91 MB/s
two-contention core 18 -> core 0 : BW 1918.06 MB/s

two-contention core 0 -> core 0 : BW 3257.56 MB/s
two-contention core 19 -> core 0 : BW 1885.03 MB/s

two-contention core 0 -> core 0 : BW 3339.64 MB/s
two-contention core 20 -> core 0 : BW 2603.06 MB/s

two-contention core 0 -> core 0 : BW 3119.29 MB/s
two-contention core 21 -> core 0 : BW 1918.6 MB/s

two-contention core 0 -> core 0 : BW 3054.14 MB/s
two-contention core 22 -> core 0 : BW 1910.61 MB/s

two-contention core 0 -> core 0 : BW 3214.44 MB/s
two-contention core 23 -> core 0 : BW 1881.69 MB/s

two-contention core 0 -> core 0 : BW 3332.3 MB/s
two-contention core 24 -> core 0 : BW 2611.8 MB/s

two-contention core 0 -> core 0 : BW 3111.94 MB/s
two-contention core 25 -> core 0 : BW 1922.11 MB/s

two-contention core 0 -> core 0 : BW 3049.02 MB/s
two-contention core 26 -> core 0 : BW 1912.85 MB/s

two-contention core 0 -> core 0 : BW 3251.88 MB/s
two-contention core 27 -> core 0 : BW 1881.82 MB/s

two-contention core 0 -> core 0 : BW 3345.6 MB/s
two-contention core 28 -> core 0 : BW 2598.82 MB/s

two-contention core 0 -> core 0 : BW 3109.04 MB/s
two-contention core 29 -> core 0 : BW 1923.81 MB/s

two-contention core 0 -> core 0 : BW 3062.94 MB/s
two-contention core 30 -> core 0 : BW 1921.3 MB/s

two-contention core 0 -> core 0 : BW 3220.8 MB/s
two-contention core 31 -> core 0 : BW 1901.76 MB/s

Si quelqu'un a d'autres améliorations, je serais heureux d'entendre parler d'eux. Par exemple, ce sont évidemment pas parfait de la bande passante des mesures en unités réelles (probablement par un--espérons constant--facteur entier).

5voto

Andre Holzner Points 6419

quelques commentaires:

  • pour voir la NUMA structure de votre système (sur linux), vous pouvez obtenir un aperçu graphique à l'aide de l' lstopo utilitaire à partir de la hwloc de la bibliothèque. En particulier, vous verrez base de numéros sont membres de qui nœud NUMA (processeur)
  • char est probablement pas le bon type de données pour mesurer la RAM maximale de débit. Je soupçonne que l'utilisation d'un 32 bits ou 64 bits type de données, vous pouvez obtenir plus de données avec le même nombre de cycles de processeur.
  • Plus généralement, vous devez également vérifier que votre mesure n'est pas limitée par la vitesse du PROCESSEUR, mais par la RAM de la vitesse. Le ramspeed utilitaire par exemple déroule la boucle interne explicitement dans une certaine mesure dans le code source:

    for(i = 0; i < blk/sizeof(UTL); i += 32) {
        b[i] = a[i];        b[i+1] = a[i+1];
        ...
        b[i+30] = a[i+30];  b[i+31] = a[i+31];
    }
    

    EDIT: sur les architectures supportées ramsmp fait même utilise des "écrits à la main' assemblée de code pour ces boucles

  • L1/L2/L3 Cache effets: Il est instructif de mesurer la bande passante en Go/s en fonction de la taille de bloc. Vous devriez voir que près de quatre vitesses différentes lors de l'augmentation de la taille de bloc correspondant à l'endroit où vous êtes la lecture des données à partir de la (des caches ou de la mémoire principale). Votre processeur semble avoir 8 Mo de Level3 (?) cache, de sorte que votre 10 Millions d'octets peuvent juste la plupart du temps rester dans le cache L3 (qui est partagé entre tous les cœurs d'un processeur).

  • Les canaux de mémoire: Votre processeur dispose de 3 canaux de mémoire. Si vos banques de mémoire sont installés, vous pouvez les exploiter (voir par exemple le manuel de la carte mère), vous pouvez exécuter plusieurs threads en même temps. J'ai vu les effets que lors de la lecture avec un seul thread, l'asymptotique de la bande passante est proche de celui d'un seul module de mémoire (par exemple, de 12,8 Go/s pour la DDR-1600), tandis que lors de l'exécution de plusieurs threads, le asymptotique de la bande passante est étroite pour le nombre de canaux mémoire de fois la bande passante d'un seul module de mémoire.

3voto

RishiD Points 595

Vous pouvez également utiliser numactl pour choisir sur quel nœud exécuter le processus et où allouer de la mémoire:

 numactl --cpubind=0 --membind=1 <process>
 

J'utilise ceci combiné avec LMbench pour obtenir des numéros de latence mémoire:

 numactl --cpubind=0 --membind=0  ./lat_mem_rd -t 512
numactl --cpubind=0 --membind=1  ./lat_mem_rd -t 512
 

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X