2011-09-07 11 views
50

¿Alguien puede dar un ejemplo o un enlace a un ejemplo que usa __builtin_prefetch en GCC (o simplemente la instrucción asm prefetcht0 en general) para obtener una ventaja de rendimiento sustancial? En particular, me gustaría que el ejemplo cumpla con los siguientes criterios:¿Ejemplos de captura previa?

  1. Es un ejemplo simple, pequeño y autónomo.
  2. Al eliminar la instrucción __builtin_prefetch, se degrada el rendimiento.
  3. Reemplazando la instrucción __builtin_prefetch con el acceso de memoria correspondiente se produce una degradación del rendimiento.

Es decir, quiero el ejemplo más corto que muestre __builtin_prefetch realizando una optimización que no se podría gestionar sin él.

Respuesta

54

Aquí es una verdadera pieza de código que he tirado fuera de un proyecto más amplio. (Lo siento, es el más corto que puedo encontrar que tuvo una aceleración notable de la captación previa). Este código realiza una transposición de datos muy grande.

Este ejemplo usa las instrucciones de captación previa de SSE, que pueden ser las mismas que las que emite GCC.

Para ejecutar este ejemplo, deberá compilar esto para x64 y tener más de 4 GB de memoria. Puede ejecutarlo con un tamaño de datos más pequeño, pero será demasiado rápido para el tiempo.

#include <iostream> 
using std::cout; 
using std::endl; 

#include <emmintrin.h> 
#include <malloc.h> 
#include <time.h> 
#include <string.h> 

#define ENABLE_PREFETCH 


#define f_vector __m128d 
#define i_ptr  size_t 
inline void swap_block(f_vector *A,f_vector *B,i_ptr L){ 
    // To be super-optimized later. 

    f_vector *stop = A + L; 

    do{ 
     f_vector tmpA = *A; 
     f_vector tmpB = *B; 
     *A++ = tmpB; 
     *B++ = tmpA; 
    }while (A < stop); 
} 
void transpose_even(f_vector *T,i_ptr block,i_ptr x){ 
    // Transposes T. 
    // T contains x columns and x rows. 
    // Each unit is of size (block * sizeof(f_vector)) bytes. 

    //Conditions: 
    // - 0 < block 
    // - 1 < x 

    i_ptr row_size = block * x; 
    i_ptr iter_size = row_size + block; 

    // End of entire matrix. 
    f_vector *stop_T = T + row_size * x; 
    f_vector *end = stop_T - row_size; 

    // Iterate each row. 
    f_vector *y_iter = T; 
    do{ 
     // Iterate each column. 
     f_vector *ptr_x = y_iter + block; 
     f_vector *ptr_y = y_iter + row_size; 

     do{ 

#ifdef ENABLE_PREFETCH 
      _mm_prefetch((char*)(ptr_y + row_size),_MM_HINT_T0); 
#endif 

      swap_block(ptr_x,ptr_y,block); 

      ptr_x += block; 
      ptr_y += row_size; 
     }while (ptr_y < stop_T); 

     y_iter += iter_size; 
    }while (y_iter < end); 
} 
int main(){ 

    i_ptr dimension = 4096; 
    i_ptr block = 16; 

    i_ptr words = block * dimension * dimension; 
    i_ptr bytes = words * sizeof(f_vector); 

    cout << "bytes = " << bytes << endl; 
// system("pause"); 

    f_vector *T = (f_vector*)_mm_malloc(bytes,16); 
    if (T == NULL){ 
     cout << "Memory Allocation Failure" << endl; 
     system("pause"); 
     exit(1); 
    } 
    memset(T,0,bytes); 

    // Perform in-place data transpose 
    cout << "Starting Data Transpose... "; 
    clock_t start = clock(); 
    transpose_even(T,block,dimension); 
    clock_t end = clock(); 

    cout << "Done" << endl; 
    cout << "Time: " << (double)(end - start)/CLOCKS_PER_SEC << " seconds" << endl; 

    _mm_free(T); 
    system("pause"); 
} 

Cuando corro con activar ENABLE_PREFETCH, esta es la salida:

bytes = 4294967296 
Starting Data Transpose... Done 
Time: 0.725 seconds 
Press any key to continue . . . 

cuando corro con ENABLE_PREFETCH discapacitados, esta es la salida:

bytes = 4294967296 
Starting Data Transpose... Done 
Time: 0.822 seconds 
Press any key to continue . . . 

Así que hay una 13% de aceleración de la captación previa.

EDIT:

Aquí hay más resultados: algunos

Operating System: Windows 7 Professional/Ultimate 
Compiler: Visual Studio 2010 SP1 
Compile Mode: x64 Release 

Intel Core i7 860 @ 2.8 GHz, 8 GB DDR3 @ 1333 MHz 
Prefetch : 0.868 
No Prefetch: 0.960 

Intel Core i7 920 @ 3.5 GHz, 12 GB DDR3 @ 1333 MHz 
Prefetch : 0.725 
No Prefetch: 0.822 

Intel Core i7 2600K @ 4.6 GHz, 16 GB DDR3 @ 1333 MHz 
Prefetch : 0.718 
No Prefetch: 0.796 

2 x Intel Xeon X5482 @ 3.2 GHz, 64 GB DDR2 @ 800 MHz 
Prefetch : 2.273 
No Prefetch: 2.666 
+0

Interesante. Desafortunadamente en las dos máquinas que probé (MacBook Pro con "Core 2 Duo" y una máquina Linux con un "Procesador AMD Opteron de Quad-Core 2376") no obtuve una diferencia significativa entre las dos versiones. Sospecho que tiene que ver con el tamaño de la caché: parece que tienes una máquina mejor que esas dos. ¿Qué piensas? –

+1

Mi máquina es Core i7 920 @ 3.5 GHz. 8 MB de caché L3. Esta aceleración del 10% es más o menos consistente en otras 3 máquinas que he probado: Core i7 2600K a 4.6 GHz y 2 x Xeon X5482 a 3.2 GHz. Pero admito que nunca lo he probado en una computadora portátil o en una máquina AMD. – Mysticial

+0

Acabo de editar mi respuesta con los puntos de referencia en las 4 máquinas que probé. Son todos escritorios/estaciones de trabajo Intel. Entonces esa podría ser la razón. No probé si su 3er punto se mantiene. Podría ser que sustituirlo por un acceso a la memoria podría producir el mismo resultado. – Mysticial

0

De the documentation:

 for (i = 0; i < n; i++) 
     { 
      a[i] = a[i] + b[i]; 
      __builtin_prefetch (&a[i+j], 1, 1); 
      __builtin_prefetch (&b[i+j], 0, 1); 
      /* ... */ 
     } 
+15

Espero que el precaptador de hardware de la CPU, haya captado previamente esto de todos modos. Esta suele ser la causa de que las personas descubran que "la captación previa no hace nada": realmente requiere que el patrón de acceso sea algo que una pieza de lógica razonablemente simple, analizando los patrones de acceso, no puede predecir. – Crowley9

+1

@ Crowley9 No estoy de acuerdo con que esta sea una mala respuesta. El OP quería un ejemplo simple (probablemente para saber cómo usarlo), esto responde a eso. – a3mlord

+0

Las CPU antiguas con una recuperación previa de hardware menos inteligente se beneficiaron de la recuperación previa de software en más casos. Sin embargo, creo que incluso P4 habría sido lo suficientemente inteligente como para HW precapturar accesos secuenciales a datos contiguos. Este es un ejemplo terrible porque es un caso en el que las instrucciones de captación previa adicional disminuyen la velocidad. @ a3mlord: El OP quería una ganancia de rendimiento, no solo la sintaxis. –

24

búsqueda binaria es un ejemplo sencillo que podría beneficiarse de la obtención previa explícita. El patrón de acceso en una búsqueda binaria parece bastante aleatorio para el captador previo de hardware, por lo que hay pocas posibilidades de que prediga con precisión qué recuperar.

En este ejemplo, prefijo las dos posibles ubicaciones "intermedias" de la siguiente iteración de ciclo en la iteración actual. Es probable que nunca se use uno de los prefetches, pero el otro lo hará (a menos que sea la iteración final).

#include <time.h> 
#include <stdio.h> 
#include <stdlib.h> 

int binarySearch(int *array, int number_of_elements, int key) { 
     int low = 0, high = number_of_elements-1, mid; 
     while(low <= high) { 
       mid = (low + high)/2; 
      #ifdef DO_PREFETCH 
      // low path 
      __builtin_prefetch (&array[(mid + 1 + high)/2], 0, 1); 
      // high path 
      __builtin_prefetch (&array[(low + mid - 1)/2], 0, 1); 
      #endif 

       if(array[mid] < key) 
         low = mid + 1; 
       else if(array[mid] == key) 
         return mid; 
       else if(array[mid] > key) 
         high = mid-1; 
     } 
     return -1; 
} 
int main() { 
    int SIZE = 1024*1024*512; 
    int *array = malloc(SIZE*sizeof(int)); 
    for (int i=0;i<SIZE;i++){ 
     array[i] = i; 
    } 
    int NUM_LOOKUPS = 1024*1024*8; 
    srand(time(NULL)); 
    int *lookups = malloc(NUM_LOOKUPS * sizeof(int)); 
    for (int i=0;i<NUM_LOOKUPS;i++){ 
     lookups[i] = rand() % SIZE; 
    } 
    for (int i=0;i<NUM_LOOKUPS;i++){ 
     int result = binarySearch(array, SIZE, lookups[i]); 
    } 
    free(array); 
    free(lookups); 
} 

Cuando compilar y ejecutar este ejemplo con DO_PREFETCH habilitado, veo una reducción del 20% en tiempo de ejecución:

$ gcc c-binarysearch.c -DDO_PREFETCH -o with-prefetch -std=c11 -O3 
$ gcc c-binarysearch.c -o no-prefetch -std=c11 -O3 

$ perf stat -e L1-dcache-load-misses,L1-dcache-loads ./with-prefetch 

    Performance counter stats for './with-prefetch': 

    356,675,702  L1-dcache-load-misses  # 41.39% of all L1-dcache hits 
    861,807,382  L1-dcache-loads            

    8.787467487 seconds time elapsed 

$ perf stat -e L1-dcache-load-misses,L1-dcache-loads ./no-prefetch 

Performance counter stats for './no-prefetch': 

    382,423,177  L1-dcache-load-misses  # 97.36% of all L1-dcache hits 
    392,799,791  L1-dcache-loads            

    11.376439030 seconds time elapsed 

en cuenta que estamos haciendo el doble de cargas de caché L1 en la versión de captación previa.De hecho, estamos trabajando mucho más, pero el patrón de acceso a la memoria es más amigable con la tubería. Esto también muestra la compensación. Si bien este bloque de código se ejecuta más rápido de forma aislada, hemos cargado una gran cantidad de basura en las memorias caché y esto puede ejercer más presión sobre otras partes de la aplicación.