Medición del ancho de banda de la memoria del producto punto de dos matrices

El producto punto de dos matrices

for(int i=0; i<n; i++) { sum += x[i]*y[i]; } 

no reutiliza los datos, por lo que debe ser una operación vinculada a la memoria. Por lo tanto, debería ser capaz de medir el ancho de banda de la memoria del producto escalar.

Usando el código en why-vectorizing-the-loop-does-not-have-performance-improvement obtengo un ancho de banda de 9.3 GB / s para mi sistema . Sin embargo, cuando bash calcular el ancho de banda usando el producto de punto, obtengo más del doble de la tasa de un único hilo y más de tres veces el índice usando múltiples hilos (mi sistema tiene cuatro núcleos / ocho hilos de hipertexto). Esto no tiene sentido para mí, ya que una operación de límite de memoria no debería beneficiarse de múltiples hilos. Aquí está el resultado del código a continuación:

 Xeon E5-1620, GCC 4.9.0, Linux kernel 3.13 dot 1 thread: 1.0 GB, sum 191054.81, time 4.98 s, 21.56 GB/s, 5.39 GFLOPS dot_avx 1 thread 1.0 GB, sum 191043.33, time 5.16 s, 20.79 GB/s, 5.20 GFLOPS dot_avx 2 threads: 1.0 GB, sum 191045.34, time 3.44 s, 31.24 GB/s, 7.81 GFLOPS dot_avx 8 threads: 1.0 GB, sum 191043.34, time 3.26 s, 32.91 GB/s, 8.23 GFLOPS 

¿Puede alguien explicarme por qué tengo más del doble del ancho de banda para un hilo y más de tres veces el ancho de banda usando más de un hilo?

Aquí está el código que utilicé:

 //g++ -O3 -fopenmp -mavx -ffast-math dot.cpp #include  #include  #include  #include  #include  #include  extern "C" inline float horizontal_add(__m256 a) { __m256 t1 = _mm256_hadd_ps(a,a); __m256 t2 = _mm256_hadd_ps(t1,t1); __m128 t3 = _mm256_extractf128_ps(t2,1); __m128 t4 = _mm_add_ss(_mm256_castps256_ps128(t2),t3); return _mm_cvtss_f32(t4); } extern "C" float dot_avx(float * __restrict x, float * __restrict y, const int n) { x = (float*)__builtin_assume_aligned (x, 32); y = (float*)__builtin_assume_aligned (y, 32); float sum = 0; #pragma omp parallel reduction(+:sum) { __m256 sum1 = _mm256_setzero_ps(); __m256 sum2 = _mm256_setzero_ps(); __m256 sum3 = _mm256_setzero_ps(); __m256 sum4 = _mm256_setzero_ps(); __m256 x8, y8; #pragma omp for for(int i=0; i<n; i+=32) { x8 = _mm256_loadu_ps(&x[i]); y8 = _mm256_loadu_ps(&y[i]); sum1 = _mm256_add_ps(_mm256_mul_ps(x8,y8),sum1); x8 = _mm256_loadu_ps(&x[i+8]); y8 = _mm256_loadu_ps(&y[i+8]); sum2 = _mm256_add_ps(_mm256_mul_ps(x8,y8),sum2); x8 = _mm256_loadu_ps(&x[i+16]); y8 = _mm256_loadu_ps(&y[i+16]); sum3 = _mm256_add_ps(_mm256_mul_ps(x8,y8),sum3); x8 = _mm256_loadu_ps(&x[i+24]); y8 = _mm256_loadu_ps(&y[i+24]); sum4 = _mm256_add_ps(_mm256_mul_ps(x8,y8),sum4); } sum += horizontal_add(_mm256_add_ps(_mm256_add_ps(sum1,sum2),_mm256_add_ps(sum3,sum4))); } return sum; } extern "C" float dot(float * __restrict x, float * __restrict y, const int n) { x = (float*)__builtin_assume_aligned (x, 32); y = (float*)__builtin_assume_aligned (y, 32); float sum = 0; for(int i=0; i<n; i++) { sum += x[i]*y[i]; } return sum; } int main(){ uint64_t LEN = 1 << 27; float *x = (float*)_mm_malloc(sizeof(float)*LEN,64); float *y = (float*)_mm_malloc(sizeof(float)*LEN,64); for(uint64_t i=0; i<LEN; i++) { x[i] = 1.0*rand()/RAND_MAX - 0.5; y[i] = 1.0*rand()/RAND_MAX - 0.5;} uint64_t size = 2*sizeof(float)*LEN; volatile float sum = 0; double dtime, rate, flops; int repeat = 100; dtime = omp_get_wtime(); for(int i=0; i<repeat; i++) sum += dot(x,y,LEN); dtime = omp_get_wtime() - dtime; rate = 1.0*repeat*size/dtime*1E-9; flops = 2.0*repeat*LEN/dtime*1E-9; printf("%f GB, sum %f, time %fs, %.2f GB/s, %.2f GFLOPS\n", 1.0*size/1024/1024/1024, sum, dtime, rate,flops); sum = 0; dtime = omp_get_wtime(); for(int i=0; i<repeat; i++) sum += dot_avx(x,y,LEN); dtime = omp_get_wtime() - dtime; rate = 1.0*repeat*size/dtime*1E-9; flops = 2.0*repeat*LEN/dtime*1E-9; printf("%f GB, sum %f, time %fs, %.2f GB/s, %.2f GFLOPS\n", 1.0*size/1024/1024/1024, sum, dtime, rate,flops); } 

Acabo de descargar, cumplir y ejecutar STREAM como lo sugirió Jonathan Dursi y aquí están los resultados:

Un hilo

 Function Rate (MB/s) Avg time Min time Max time Copy: 14292.1657 0.0023 0.0022 0.0023 Scale: 14286.0807 0.0023 0.0022 0.0023 Add: 14724.3906 0.0033 0.0033 0.0033 Triad: 15224.3339 0.0032 0.0032 0.0032 

Ocho hilos

 Function Rate (MB/s) Avg time Min time Max time Copy: 24501.2282 0.0014 0.0013 0.0021 Scale: 23121.0556 0.0014 0.0014 0.0015 Add: 25263.7209 0.0024 0.0019 0.0056 Triad: 25817.7215 0.0020 0.0019 0.0027 

Hay algunas cosas que pasan aquí, que se reducen a:

  • Tienes que trabajar bastante duro para sacar hasta el último bit de rendimiento del subsistema de memoria; y
  • Diferentes puntos de referencia miden cosas diferentes.

La primera ayuda a explicar por qué necesita varios subprocesos para saturar el ancho de banda de memoria disponible. Hay mucha concurrencia en el sistema de memoria, y aprovecharlo a menudo requerirá cierta concurrencia en su código de CPU. Una razón importante por la que varios hilos de ejecución ayudan a ocultar la latencia : mientras un hilo se detiene esperando a que lleguen los datos, otro hilo puede aprovechar otros datos que acaban de estar disponibles.

En este caso, el hardware te ayuda mucho en un único hilo: como el acceso a la memoria es tan predecible, el hardware puede captar previamente los datos antes que cuando los necesites, lo que te brinda la ventaja de ocultar la latencia incluso con un hilo; pero hay límites a lo que puede hacer precaptura. El captador previo no se encargará de cruzar los límites de la página, por ejemplo. La referencia canónica para gran parte de esto es Lo que todo progtwigdor debe saber sobre la memoria por Ulrich Drepper , que ahora es lo suficientemente viejo como para comenzar a mostrar algunas lagunas (la vista general de chips de Intel de su procesador Sandy Bridge está aquí ; nótese especialmente la integración más estrecha del hardware de gestión de memoria con la CPU).

En cuanto a la pregunta acerca de comparar con memset, mbw o STREAM , la comparación entre los puntos de referencia siempre causará dolores de cabeza, incluso puntos de referencia que afirman estar midiendo la misma cosa. En particular, el “ancho de banda de la memoria” no es un número único: el rendimiento varía bastante dependiendo de las operaciones. Tanto mbw como Stream hacen alguna versión de una operación de copia, con operaciones de STREAM explicadas aquí (tomadas directamente de la página web, todos los operandos son puntos flotantes de precisión doble):

 ------------------------------------------------------------------ name kernel bytes/iter FLOPS/iter ------------------------------------------------------------------ COPY: a(i) = b(i) 16 0 SCALE: a(i) = q*b(i) 16 1 SUM: a(i) = b(i) + c(i) 24 1 TRIAD: a(i) = b(i) + q*c(i) 24 2 ------------------------------------------------------------------ 

así que aproximadamente 1 / 2-1 / 3 de las operaciones de memoria en estos casos son escrituras (y todo es una escritura en el caso de memset). Si bien las escrituras individuales pueden ser un poco más lentas que las lecturas, el problema más grande es que es mucho más difícil saturar el subsistema de memoria con escrituras porque, por supuesto, no se puede hacer el equivalente a la captación previa de una escritura. Intercalar las lecturas y escrituras ayuda, pero su ejemplo de producto de punto, que básicamente es todo lo que se lee, será sobre el mejor caso posible para vincular la aguja en el ancho de banda de la memoria.

Además, el benchmark STREAM está (intencionalmente) escrito completamente portátil, con solo algunos pragmas de comstackción para sugerir la vectorización, por lo que superar el benchmark STREAM no es necesariamente una señal de advertencia, especialmente cuando lo que estás haciendo son dos lecturas de transmisión.

Hice mi propio código de referencia de memoria https://github.com/zboson/bandwidth

Aquí están los resultados actuales para ocho hilos:

 write: 0.5 GB, time 2.96e-01 s, 18.11 GB/s copy: 1 GB, time 4.50e-01 s, 23.85 GB/s scale: 1 GB, time 4.50e-01 s, 23.85 GB/s add: 1.5 GB, time 6.59e-01 s, 24.45 GB/s mul: 1.5 GB, time 6.56e-01 s, 24.57 GB/s triad: 1.5 GB, time 6.61e-01 s, 24.37 GB/s vsum: 0.5 GB, time 1.49e-01 s, 36.09 GB/s, sum -8.986818e+03 vmul: 0.5 GB, time 9.00e-05 s, 59635.10 GB/s, sum 0.000000e+00 vmul_sum: 1 GB, time 3.25e-01 s, 33.06 GB/s, sum 1.910421e+04 

Aquí están los resultados de streams para 1 hilo:

 write: 0.5 GB, time 4.65e-01 s, 11.54 GB/s copy: 1 GB, time 7.51e-01 s, 14.30 GB/s scale: 1 GB, time 7.45e-01 s, 14.41 GB/s add: 1.5 GB, time 1.02e+00 s, 15.80 GB/s mul: 1.5 GB, time 1.07e+00 s, 15.08 GB/s triad: 1.5 GB, time 1.02e+00 s, 15.76 GB/s vsum: 0.5 GB, time 2.78e-01 s, 19.29 GB/s, sum -8.990941e+03 vmul: 0.5 GB, time 1.15e-05 s, 468719.08 GB/s, sum 0.000000e+00 vmul_sum: 1 GB, time 5.72e-01 s, 18.78 GB/s, sum 1.910549e+04 
  1. write: escribe una constante (3.14159) en una matriz. Esto debería ser como memset .
  2. copiar, escalar, agregar y tríada se definen de la misma manera que en STREAM
  3. mul: a(i) = b(i) * c(i)
  4. vsum: sum += a(i)
  5. vmul: sum *= a(i)
  6. vmul_sum: sum += a(i)*b(i) // el producto de puntos

Mis resultados son consistentes con STREAM. Obtengo el ancho de banda más alto para vsum . El método vmul no funciona actualmente (una vez que el valor es cero, termina temprano). Puedo obtener resultados ligeramente mejores (en aproximadamente un 10%) usando intrínsecos y desenrollando el ciclo que añadiré más adelante.