c++ - que - Medición de NUMA(acceso a memoria no uniforme). Sin asimetría observable. ¿Por qué?
que es numa en procesadores (6)
Ah, ja! Mysticial tiene razón! De alguna manera, la precarga de hardware optimiza mis lecturas / escrituras.
Si se tratara de una optimización de caché, forzar una barrera de memoria vencería a la optimización:
c = __sync_fetch_and_add(((char*)x) + j, 1);
pero eso no hace ninguna diferencia. Lo que hace la diferencia es multiplicar mi índice de iterador por el primo 1009 para vencer la optimización de precarga:
*(((char*)x) + ((j * 1009) % N)) += 1;
Con ese cambio, la asimetría NUMA se revela claramente:
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
Al menos creo que eso es lo que está pasando.
Gracias Mysticial!
EDITAR: CONCLUSIÓN ~ 133%
Para cualquiera que esté echando un vistazo a esta publicación para tener una idea aproximada de las características de rendimiento de NUMA, aquí está la conclusión de acuerdo con mis pruebas:
El acceso de memoria a un nodo NUMA no local tiene aproximadamente 1,33 veces la latencia de acceso a memoria a un nodo local.
Intenté medir los efectos asimétricos de acceso a memoria de NUMA y fallé.
El experimento
Realizado en un Intel Xeon X5570 @ 2.93 GHz, 2 CPUs, 8 núcleos.
En una hebra anclada al núcleo 0, asigno una matriz x de tamaño 10,000,000 bytes en el nodo NUMA del núcleo 0 con numa_alloc_local. Luego itero sobre la matriz x 50 veces y leo y escribo cada byte en la matriz. Mida el tiempo transcurrido para hacer las 50 iteraciones.
Luego, en cada uno de los otros núcleos en mi servidor, pin un nuevo hilo y otra vez mida el tiempo transcurrido para hacer 50 iteraciones de lectura y escritura en cada byte en el conjunto x .
La matriz x es grande para minimizar los efectos de caché. Queremos medir la velocidad cuando la CPU tiene que recorrer todo el camino hasta la RAM para cargar y almacenar, no cuando las cachés están ayudando.
Hay dos nodos NUMA en mi servidor, por lo que esperaría que los núcleos que tienen afinidad en el mismo nodo en el que se asigna la matriz x tengan una velocidad de lectura / escritura más rápida. No estoy viendo eso.
¿Por qué?
Quizás NUMA solo sea relevante en sistemas con> 8-12 núcleos, como he visto sugerido en otro lugar.
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 salida
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
Hacer 50 iteraciones leyendo y escribiendo sobre la matriz x toma aproximadamente 1,7 segundos, sin importar qué núcleo está haciendo la lectura y la escritura.
Actualizar:
El tamaño de la memoria caché en mi CPU es de 8Mb, por lo que quizás 10Mb array x no sea lo suficientemente grande como para eliminar los efectos de caché. Probé 100Mb array x , y he intentado emitir una cerca de memoria completa con __sync_synchronize () dentro de mis bucles más internos. Todavía no revela ninguna asimetría entre los nodos NUMA.
Actualización 2:
Intenté leer y escribir en array x con __sync_fetch_and_add (). Aún nada.
Gracias por este código de referencia. Tomé su versión "fija" y la cambié por C + OpenMP puro y agregué algunas pruebas de cómo se comporta el sistema de memoria bajo contención. Puedes encontrar el nuevo código here .
Aquí hay algunos resultados de muestra de un Quad Opteron:
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 alguien tiene más mejoras, me gustaría saber de ellos. Por ejemplo, obviamente estas no son mediciones de ancho de banda perfectas en unidades del mundo real (probablemente por un factor entero - con suerte constante -).
Lo primero que quiero señalar es que es posible que desee comprobar qué núcleos están en cada nodo. No recuerdo núcleos y nodos intercalados así. Además, debe tener 16 hilos debido a HT. (a menos que lo haya deshabilitado)
Otra cosa:
Las máquinas socket Xeon 1366 son solo ligeramente NUMA. Entonces será difícil ver la diferencia. El efecto NUMA es mucho más notable en los Opterons 4P.
En sistemas como el suyo, el ancho de banda de nodo a nodo es realmente más rápido que el ancho de banda de la CPU a la memoria. Como su patrón de acceso es completamente secuencial, obtiene el ancho de banda completo, independientemente de si los datos son locales o no. Una mejor cosa para medir es la latencia. Pruebe el acceso aleatorio a un bloque de 1 GB en lugar de reproducirlo secuencialmente.
Última cosa:
Dependiendo de qué tan agresivamente optimice su compilador, su bucle podría estar optimizado ya que no hace nada:
c = ((char*)x)[j];
((char*)x)[j] = c;
Algo como esto garantizará que el compilador no lo elimine:
((char*)x)[j] += 1;
Si alguien más quiere probar esta prueba, aquí está el programa de trabajo modificado. Me encantaría ver los resultados de otro hardware. Esto funciona en mi máquina con Linux 2.6.34-12-desktop, GCC 4.5.0, Boost 1.47.
g++ -o numatest -pthread -lboost_thread -lnuma -O0 numatest.cpp
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)
{
*(((char*)y) + ((j * 1009) % N)) += 1;
}
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)
{
*(((char*)x) + ((j * 1009) % N)) += 1;
}
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(5);
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;
}
También puede usar numactl para elegir en qué nodo ejecutar el proceso y dónde asignar la memoria desde:
numactl --cpubind=0 --membind=1 <process>
Utilizo esto combinado con LMbench para obtener números de latencia de memoria:
numactl --cpubind=0 --membind=0 ./lat_mem_rd -t 512
numactl --cpubind=0 --membind=1 ./lat_mem_rd -t 512
algunos comentarios:
- para ver la estructura NUMA de su sistema (en Linux), puede obtener una descripción gráfica utilizando la utilidad
lstopo
de la biblioteca hwloc . En particular, verá qué números principales son miembros de qué nodo NUMA (socket del procesador) -
char
probablemente no sea el tipo de datos ideal para medir el rendimiento máximo de RAM. Sospecho que con un tipo de datos de 32 o 64 bits, puede obtener más datos con el mismo número de ciclos de CPU. De manera más general, también debe verificar que su medición no esté limitada por la velocidad de la CPU, sino por la velocidad de la RAM. La utilidad de ramspeed , por ejemplo, desenrolla el ciclo interno explícitamente en cierta medida en el código fuente:
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]; }
EDITAR : en arquitecturas soportadas,
ramsmp
realidad incluso usa código de ensamblaje "escrito a mano" para estos buclesEfectos de caché L1 / L2 / L3: es instructivo medir el ancho de banda en GByte / s en función del tamaño del bloque. Debería ver que aproximadamente cuatro velocidades diferentes aumentan el tamaño de bloque correspondiente a donde está leyendo los datos (memorias caché o memoria principal). Su procesador parece tener 8 MByte de caché Level3 (?), Por lo que sus 10 millones de bytes pueden permanecer en la caché L3 (que se comparte entre todos los núcleos de un procesador).
Canales de memoria : su procesador tiene 3 canales de memoria . Si sus bancos de memoria están instalados de manera que pueda explotarlos todos (consulte, por ejemplo, el manual de la placa base), es posible que desee ejecutar más de un hilo al mismo tiempo. Vi efectos que al leer con un solo subproceso, el ancho de banda asintótico es similar al de un solo módulo de memoria (por ejemplo, 12,8 GByte / s para DDR-1600), mientras que cuando se ejecutan varios subprocesos, el ancho de banda asintótico es similar al número de canales de memoria por el ancho de banda de un solo módulo de memoria.