21 votos

Escalable asignación de los grandes (8 MB) de memoria de las regiones en las arquitecturas NUMA

Actualmente estamos utilizando un TBB gráfico de flujo en los que a) un paralelo filtro de los procesos de una matriz (en paralelo con offsets) y pone a los resultados procesados en un intermedio vector (asignado en el montón, sobre todo el vector va a crecer hasta 8 mb). Estos vectores se pasa a continuación a los nodos que luego postproceso estos resultados con base en sus características (determinado en a)). Porque de sincronizada de los recursos, no sólo puede ser un nodo para cada característica. El prototipo que escribió funciona bien en la UMA arquitecturas (probado en una sola CPU Ivy Bridge y Sandy Bridge arquitectura). Sin embargo, la aplicación no se escala en nuestra arquitectura NUMA (4 CPU Nehalem-EX). Hemos depositado el problema de asignación de memoria y creó un mínimo ejemplo en el que tenemos un paralelo con la tubería que sólo asigna memoria del montón (a través de malloc de un 8MB trozo, luego memset la 8MB región; similar a lo que el prototipo inicial que haría) hasta una cierta cantidad de memoria. Nuestras conclusiones son las siguientes:

  • En una UMA de la arquitectura de la aplicación se escala linealmente con el número de hilos utilizados por la tubería (establecido a través de task_scheduler_init)

  • En la arquitectura NUMA cuando nos pin de la aplicación a una toma de corriente (utilizando numactl) vemos la misma escala lineal-up

  • En el NUMA architecutre cuando utilizamos más de una toma, el tiempo que nuestra aplicación se ejecuta aumenta con el número de sockets (negativo de la escala lineal-"up")

Para nosotros esto huele montón de contención. Lo que hemos tratado hasta el momento es el sustituto de Intel"s TBB escalable asignador para glibc asignador. Sin embargo, el rendimiento inicial en un único socket es peor que el uso de glibc, en enchufes múltiples de rendimiento no está empeorando, pero tampoco mejor. Hemos obtenido el mismo efecto mediante tcmalloc, la horda asignador, y TBB caché del alineados asignador.

La pregunta es si alguien a tenido problemas similares. En la pila de la asignación no es una opción para nosotros porque queremos mantener la asigna el montón de vectores incluso después de que la tubería se ejecutó. ¿Cómo puede un montón de asignar regiones de memoria en el tamaño de MBs de manera eficiente en NUMA arquitecturas de múltiples hilos? Que realmente nos gusta mantener una asignación dinámica de acercamiento en lugar de preallocating la memoria y la gestión de las ti dentro de la aplicación.

Os adjunto perf estadísticas para las diferentes ejecuciones con numactl. Intercalado/localalloc no tiene ningún efecto (el bus QPI no es el cuello de botella; hemos comprobado que con PCM, QPI link de la carga está en el 1%). También he añadido un gráfico que representa los resultados de glibc, tbbmalloc, y tcmalloc.

perf stat bin/prototipo 598.867

Contador de rendimiento de las estadísticas de 'bin/prototipo':

  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/prototipo 272.614

Contador de rendimiento de las estadísticas de 'numactl --cpunodebind=0 bin/prototipo':

   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/prototipo 356.726

Contador de rendimiento de las estadísticas de 'numactl --cpunodebind=0-1 bin/prototipo':

   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

Contador de rendimiento de las estadísticas de 'numactl --cpunodebind=0-2 bin/prototipo':

   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 CPU de unión que tenemos el siguiente hallazgo: 1 CPU (velocidad x), 2 Cpu (velocidad ~ x/2), 3 Cpu (velocidad ~ x/3) [velocidad=el más alto mejor]. Así que lo que vemos es que el rendimiento empeora con el número de CPUs. La memoria vinculante, intercalado (--interleave=todos) y --localalloc no tienen ningún efecto aquí (hicimos un seguimiento de todos los enlaces QPI y carga de enlace estaba por debajo de 1% por 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;
}

La discusión sobre Intel TBB foros: http://software.intel.com/en-us/forums/topic/346334

3voto

muehlbau Puntos 888

Una breve actualización y una respuesta parcial para el problema descrito: La llamada a malloc o scalable_malloc no son el cuello de botella, el cuello de botella son más bien los errores de página provocada por memsetting de la memoria asignada. No hay ninguna diferencia entre glibc malloc y otros escalable asignadores tales como Intel TBB scalable_malloc: para las asignaciones mayor que un umbral determinado (generalmente de 1 MB si nada es freed; puede ser definido por madvise) la memoria se asigna una anoymous mmap. Inicialmente todas las páginas de un punto del mapa a un núcleo interneal página que es pre-0ed y de sólo lectura. Cuando nos memset la memoria, esto desencadena una excepción (la mente el núcleo de la página es de sólo lectura) y un fallo de página. Una nueva página será 0ed en este momento. Pequeñas páginas de 4 kb así que esto va a suceder 2048 veces para el 8MB de buffer de asignar y escribir. Lo que se mide es que estos errores de página no son tan caros en un único zócalo de máquinas, sino de conseguir más y más caro en NUMA máquinas con múltiples CPUs.

Las soluciones que se me ocurrió hasta ahora:

  • El uso de enorme páginas: ayuda, pero sólo retrasa el problema

  • El uso de un preasignados y pre-falla ( memset o mmap + MAP_POPULATE) región de memoria (memoria de la piscina) y asignar desde allí: ayuda pero no necesariamente quiere hacer que

  • Frente a este problema de escalabilidad en el kernel de Linux

1voto

muehlbau Puntos 888

Segunda Actualización (cierre de la pregunta):

Sólo un perfil de la aplicación de ejemplo de nuevo con un kernel 3.10.

Resultados en paralelo a la asignación y memsetting de 16GB de datos:

páginas pequeñas:

  • 1 socket: 3112.29 ms
  • 2 socket: 2965.32 ms
  • 3 socket: 3000.72 ms
  • 4 socket: 3211.54 ms

enorme páginas:

  • 1 socket: 3086.77 ms
  • 2 socket: 1568.43 ms
  • 3 socket: 1084.45 ms
  • 4 socket: 852.697 ms

El escalable asignación problema parece estar solucionado ahora - al menos para la gran páginas.

Iteramos.com

Iteramos es una comunidad de desarrolladores que busca expandir el conocimiento de la programación mas allá del inglés.
Tenemos una gran cantidad de contenido, y también puedes hacer tus propias preguntas o resolver las de los demás.

Powered by:

X