c++ memory-management parallel-processing tbb numa

c++ - Asignación escalable de regiones de memoria grandes(8 MB) en arquitecturas NUMA



memory-management parallel-processing (2)

Actualmente estamos utilizando un gráfico de flujo de TBB en el que a) un filtro paralelo procesa una matriz (en paralelo con las compensaciones) y coloca los resultados procesados ​​en un vector intermedio (asignado en el montón; principalmente, el vector crecerá hasta 8 MB). Estos vectores se pasan luego a los nodos que luego procesan estos resultados en función de sus características (determinadas en a)). Debido a los recursos sincronizados, solo puede haber un nodo de este tipo para cada característica. El prototipo que escribimos funciona bien en arquitecturas UMA (probado en una sola CPU Ivy Bridge y Sandy Bridge). Sin embargo, la aplicación no se escala en nuestra arquitectura NUMA (4 CPU Nehalem-EX). Detectamos el problema en la asignación de memoria y creamos un ejemplo mínimo en el que tenemos una tubería paralela que simplemente asigna memoria del montón (a través de malloc de una porción de 8MB, luego configuramos la región de 8MB; similar a lo que haría el prototipo inicial) Hasta cierta cantidad de memoria. Nuestros hallazgos son:

  • En una arquitectura UMA, la aplicación se amplía linealmente con el número de subprocesos utilizados por la canalización (establecido a través de task_scheduler_init)

  • En la arquitectura NUMA cuando conectamos la aplicación a un socket (usando numactl) vemos la misma escala lineal

  • En el architecutre NUMA cuando usamos más de un socket, el tiempo que nuestra aplicación se ejecuta aumenta con el número de sockets (escala lineal negativa - "arriba")

Para nosotros esto huele a contención del montón. Lo que hemos intentado hasta ahora es sustituir el asignador escalable TBB de Intel por el asignador glibc. Sin embargo, el rendimiento inicial en un solo socket es peor que usar glibc, en el rendimiento de múltiples sockets no está empeorando, pero tampoco está mejorando. obtuvo el mismo efecto utilizando tcmalloc, el asignador de acumulación y el asignador alineado de caché de TBB.

La pregunta es si alguien experimentó problemas similares. La asignación de pila no es una opción para nosotros, ya que queremos mantener los vectores asignados al montón incluso después de que se ejecutó la tubería. ¿Cómo puede un montón asignar regiones de memoria en el tamaño de los MB de manera eficiente en arquitecturas NUMA desde varios subprocesos? Realmente nos gustaría mantener un enfoque de asignación dinámica en lugar de preasignar la memoria y administrarla dentro de la aplicación.

Adjunté perf stats para las diferentes ejecuciones con numactl. Interleaving / localalloc no tiene ningún efecto (el bus QPI no es el cuello de botella; verificamos que con PCM, la carga del enlace QPI está al 1%). También agregué una tabla que muestra los resultados de glibc, tbbmalloc y tcmalloc.

Perf stat bin / prototipo 598.867

Estadísticas de contador de rendimiento para ''bin / prototype'':

12965,118733 task-clock # 7,779 CPUs utilized 10.973 context-switches # 0,846 K/sec 1.045 CPU-migrations # 0,081 K/sec 284.210 page-faults # 0,022 M/sec 17.266.521.878 cycles # 1,332 GHz [82,84%] 15.286.104.871 stalled-cycles-frontend # 88,53% frontend cycles idle [82,84%] 10.719.958.132 stalled-cycles-backend # 62,09% backend cycles idle [67,65%] 3.744.397.009 instructions # 0,22 insns per cycle # 4,08 stalled cycles per insn [84,40%] 745.386.453 branches # 57,492 M/sec [83,50%] 26.058.804 branch-misses # 3,50% of all branches [83,33%] 1,666595682 seconds time elapsed

perf stat numactl --cpunodebind = 0 bin / prototype 272.614

Estadísticas del contador de rendimiento para ''numactl --cpunodebind = 0 bin / prototype'':

3887,450198 task-clock # 3,345 CPUs utilized 2.360 context-switches # 0,607 K/sec 208 CPU-migrations # 0,054 K/sec 282.794 page-faults # 0,073 M/sec 8.472.475.622 cycles # 2,179 GHz [83,66%] 7.405.805.964 stalled-cycles-frontend # 87,41% frontend cycles idle [83,80%] 6.380.684.207 stalled-cycles-backend # 75,31% backend cycles idle [66,90%] 2.170.702.546 instructions # 0,26 insns per cycle # 3,41 stalled cycles per insn [85,07%] 430.561.957 branches # 110,757 M/sec [82,72%] 16.758.653 branch-misses # 3,89% of all branches [83,06%] 1,162185180 seconds time elapsed

perf stat numactl --cpunodebind = 0-1 bin / prototype 356.726

Estadísticas del contador de rendimiento para ''numactl --cpunodebind = 0-1 bin / prototype'':

6127,077466 task-clock # 4,648 CPUs utilized 4.926 context-switches # 0,804 K/sec 469 CPU-migrations # 0,077 K/sec 283.291 page-faults # 0,046 M/sec 10.217.787.787 cycles # 1,668 GHz [82,26%] 8.944.310.671 stalled-cycles-frontend # 87,54% frontend cycles idle [82,54%] 7.077.541.651 stalled-cycles-backend # 69,27% backend cycles idle [68,59%] 2.394.846.569 instructions # 0,23 insns per cycle # 3,73 stalled cycles per insn [84,96%] 471.191.796 branches # 76,903 M/sec [83,73%] 19.007.439 branch-misses # 4,03% of all branches [83,03%] 1,318087487 seconds time elapsed

perf stat numactl --cpunodebind = 0-2 bin / protoype 472.794

Estadísticas del contador de rendimiento para ''numactl --cpunodebind = 0-2 bin / prototype'':

9671,244269 task-clock # 6,490 CPUs utilized 7.698 context-switches # 0,796 K/sec 716 CPU-migrations # 0,074 K/sec 283.933 page-faults # 0,029 M/sec 14.050.655.421 cycles # 1,453 GHz [83,16%] 12.498.787.039 stalled-cycles-frontend # 88,96% frontend cycles idle [83,08%] 9.386.588.858 stalled-cycles-backend # 66,81% backend cycles idle [66,25%] 2.834.408.038 instructions # 0,20 insns per cycle # 4,41 stalled cycles per insn [83,44%] 570.440.458 branches # 58,983 M/sec [83,72%] 22.158.938 branch-misses # 3,88% of all branches [83,92%] 1,490160954 seconds time elapsed

Ejemplo mínimo: compilado con g ++ - 4.7 std = c ++ 11 -O3 -march = native; ejecutado con numactl --cpunodebind = 0 ... numactl --cpunodebind = 0-3 - con el enlace de la CPU tenemos el siguiente hallazgo: 1 CPU (velocidad x), 2 CPU (velocidad ~ x / 2), 3 CPU (velocidad ~ x / 3) [velocidad = cuanto más alto, mejor]. Entonces, lo que vemos es que el rendimiento empeora con el número de CPU. El enlace de memoria, el intercalado (--interleave = all) y --localalloc no tienen ningún efecto aquí (monitoreamos todos los enlaces QPI y la carga del enlace fue inferior al 1% para cada enlace).

#include <tbb/pipeline.h> #include <tbb/task_scheduler_init.h> #include <chrono> #include <stdint.h> #include <iostream> #include <fcntl.h> #include <sstream> #include <sys/mman.h> #include <tbb/scalable_allocator.h> #include <tuple> namespace { // 8 MB size_t chunkSize = 8 * 1024 * 1024; // Number of threads (0 = automatic) uint64_t threads=0; } using namespace std; typedef chrono::duration<double, milli> milliseconds; int main(int /* argc */, char** /* argv */) { chrono::time_point<chrono::high_resolution_clock> startLoadTime = chrono::high_resolution_clock::now(); tbb::task_scheduler_init init(threads==0?tbb::task_scheduler_init::automatic:threads); const uint64_t chunks=128; uint64_t nextChunk=0; tbb::parallel_pipeline(128,tbb::make_filter<void,uint64_t>( tbb::filter::serial,[&](tbb::flow_control& fc)->uint64_t { uint64_t chunk=nextChunk++; if(chunk==chunks) fc.stop(); return chunk; }) & tbb::make_filter<uint64_t,void>( tbb::filter::parallel,[&](uint64_t /* item */)->void { void* buffer=scalable_malloc(chunkSize); memset(buffer,0,chunkSize); })); chrono::time_point<chrono::high_resolution_clock> endLoadTime = chrono::high_resolution_clock::now(); milliseconds loadTime = endLoadTime - startLoadTime; cout << loadTime.count()<<endl; }

Discusión en los foros de Intel TBB: http://software.intel.com/en-us/forums/topic/346334


Una actualización breve y una respuesta parcial para el problema descrito: la llamada a malloc o scalable_malloc no son el cuello de botella, sino el cuello de botella son las fallas de página provocadas por la memoria asignada. No hay diferencia entre glibc malloc y otros asignadores escalables, como el TBB scalable_malloc Intel: para asignaciones más grandes que un umbral específico (generalmente 1MB si no hay nada free ; puede ser definido por madvise ) la memoria será asignada por un mmap anónimo. Inicialmente, todas las páginas del mapa apuntan a una página interna del núcleo que está precargada y es de solo lectura. Cuando configuramos la memoria, esto desencadena una excepción (ya que la página del núcleo es de solo lectura) y un error de página. Una nueva página será publicada en este momento. Las páginas pequeñas tienen 4KB, por lo que esto ocurrirá 2048 veces para el búfer de 8MB que asignamos y escribimos. Lo que medí es que estas fallas de página no son tan caras en las máquinas de un solo zócalo, pero se vuelven más caras en las máquinas NUMA con múltiples CPU.

Soluciones que encontré hasta ahora:

  • Usa páginas enormes: ayuda pero solo retrasa el problema

  • Use una región de memoria (agrupación de memoria) preasignada y predeterminada ( memset o mmap + MAP_POPULATE ) y asigne desde allí: ayuda, pero una no necesariamente quiere hacer eso

  • Solucione este problema de escalabilidad en el kernel de Linux


Segunda actualización (cerrando la pregunta):

Simplemente perfile la aplicación de ejemplo de nuevo con un kernel 3.10.

Resultados para la asignación paralela y el montaje de 16 GB de datos:

páginas pequeñas:

  • 1 zócalo: 3112.29 ms
  • 2 zócalos: 2965.32 ms
  • 3 zócalos: 3000.72 ms
  • 4 zócalos: 3211.54 ms

páginas enormes:

  • 1 zócalo: 3086.77 ms
  • 2 zócalos: 1568.43 ms
  • 3 zócalos: 1084.45 ms
  • 4 zócalos: 852.697 ms

El problema de asignación escalable parece haberse solucionado ahora, al menos para páginas grandes.