c++ - SSE-copy, AVX-copy y std:: copy performance
simd (5)
Intenté mejorar el rendimiento de la operación de copia a través de SSE y AVX:
#include <immintrin.h>
const int sz = 1024;
float *mas = (float *)_mm_malloc(sz*sizeof(float), 16);
float *tar = (float *)_mm_malloc(sz*sizeof(float), 16);
float a=0;
std::generate(mas, mas+sz, [&](){return ++a;});
const int nn = 1000;//Number of iteration in tester loops
std::chrono::time_point<std::chrono::system_clock> start1, end1, start2, end2, start3, end3;
//std::copy testing
start1 = std::chrono::system_clock::now();
for(int i=0; i<nn; ++i)
std::copy(mas, mas+sz, tar);
end1 = std::chrono::system_clock::now();
float elapsed1 = std::chrono::duration_cast<std::chrono::microseconds>(end1-start1).count();
//SSE-copy testing
start2 = std::chrono::system_clock::now();
for(int i=0; i<nn; ++i)
{
auto _mas = mas;
auto _tar = tar;
for(; _mas!=mas+sz; _mas+=4, _tar+=4)
{
__m128 buffer = _mm_load_ps(_mas);
_mm_store_ps(_tar, buffer);
}
}
end2 = std::chrono::system_clock::now();
float elapsed2 = std::chrono::duration_cast<std::chrono::microseconds>(end2-start2).count();
//AVX-copy testing
start3 = std::chrono::system_clock::now();
for(int i=0; i<nn; ++i)
{
auto _mas = mas;
auto _tar = tar;
for(; _mas!=mas+sz; _mas+=8, _tar+=8)
{
__m256 buffer = _mm256_load_ps(_mas);
_mm256_store_ps(_tar, buffer);
}
}
end3 = std::chrono::system_clock::now();
float elapsed3 = std::chrono::duration_cast<std::chrono::microseconds>(end3-start3).count();
std::cout<<"serial - "<<elapsed1<<", SSE - "<<elapsed2<<", AVX - "<<elapsed3<<"/nSSE gain: "<<elapsed1/elapsed2<<"/nAVX gain: "<<elapsed1/elapsed3;
_mm_free(mas);
_mm_free(tar);
Funciona. Sin embargo, mientras que el número de iteraciones en los bucles de prueba - nn - aumenta, la ganancia de rendimiento de simd-copy disminuye:
nn = 10: SSE-ganancia = 3, AVX-ganancia = 6;
nn = 100: SSE-ganancia = 0.75, AVX-ganancia = 1.5;
nn = 1000: SSE-ganancia = 0.55, AVX-ganancia = 1.1;
¿Alguien puede explicar cuál es la razón del efecto de disminución de rendimiento mencionado? ¿Es recomendable vectorizar manualmente la operación de copia?
Creo que esto se debe a que la medición no es precisa para operaciones un poco cortas.
Al medir el rendimiento en la CPU Intel
Desactiva "Turbo Boost" y "SpeedStep". Puede hacerlo en el BIOS del sistema.
Cambie la prioridad del proceso / hilo a alta o en tiempo real. Esto mantendrá su hilo en ejecución.
Establezca la Máscara de CPU de proceso en un solo núcleo. El enmascaramiento de la CPU con mayor prioridad minimizará el cambio de contexto.
usar __rdtsc () función intrínseca. La serie Intel Core devuelve el contador de reloj interno de la CPU con __rdtsc (). Obtendrá 3400000000 conteos / segundo desde la CPU de 3.4 Ghz. Y __rdtsc () vacía todas las operaciones programadas en la CPU para que pueda medir el tiempo de manera más precisa.
Este es el código de inicio de mi banco de pruebas para probar los códigos SSE / AVX.
int GetMSB(DWORD_PTR dwordPtr)
{
if(dwordPtr)
{
int result = 1;
#if defined(_WIN64)
if(dwordPtr & 0xFFFFFFFF00000000) { result += 32; dwordPtr &= 0xFFFFFFFF00000000; }
if(dwordPtr & 0xFFFF0000FFFF0000) { result += 16; dwordPtr &= 0xFFFF0000FFFF0000; }
if(dwordPtr & 0xFF00FF00FF00FF00) { result += 8; dwordPtr &= 0xFF00FF00FF00FF00; }
if(dwordPtr & 0xF0F0F0F0F0F0F0F0) { result += 4; dwordPtr &= 0xF0F0F0F0F0F0F0F0; }
if(dwordPtr & 0xCCCCCCCCCCCCCCCC) { result += 2; dwordPtr &= 0xCCCCCCCCCCCCCCCC; }
if(dwordPtr & 0xAAAAAAAAAAAAAAAA) { result += 1; }
#else
if(dwordPtr & 0xFFFF0000) { result += 16; dwordPtr &= 0xFFFF0000; }
if(dwordPtr & 0xFF00FF00) { result += 8; dwordPtr &= 0xFF00FF00; }
if(dwordPtr & 0xF0F0F0F0) { result += 4; dwordPtr &= 0xF0F0F0F0; }
if(dwordPtr & 0xCCCCCCCC) { result += 2; dwordPtr &= 0xCCCCCCCC; }
if(dwordPtr & 0xAAAAAAAA) { result += 1; }
#endif
return result;
}
else
{
return 0;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
// Set Core Affinity
DWORD_PTR processMask, systemMask;
GetProcessAffinityMask(GetCurrentProcess(), &processMask, &systemMask);
SetProcessAffinityMask(GetCurrentProcess(), 1 << (GetMSB(processMask) - 1) );
// Set Process Priority. you can use REALTIME_PRIORITY_CLASS.
SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
DWORD64 start, end;
start = __rdtsc();
// your code here.
end = __rdtsc();
printf("%I64d/n", end - start);
return 0;
}
Creo que su principal problema / cuello de botella es su _mm_malloc
.
Recomiendo encarecidamente usar std::vector
como su estructura de datos principal si le preocupa la localidad en C ++.
Los intrínsecos no son exactamente una "biblioteca", son más como una función incorporada que le proporciona su compilador, debe estar familiarizado con los documentos internos de su compilador antes de usar estas funciones.
También tenga en cuenta que el hecho de que el AVX
sea más nuevo que el SSE
no lo hace más rápido, independientemente de lo que planee usar, la cantidad de ciclos que realiza una función es probablemente más importante que el argumento "avx vs sse", por ejemplo. Ejemplo de ver esta respuesta .
Pruebe con una int array[]
POD int array[]
o un std::vector
.
El problema es que su prueba hace un mal trabajo para migrar algunos factores en el hardware que dificultan la evaluación comparativa. Para probar esto, he hecho mi propio caso de prueba. Algo como esto:
for blah blah:
sleep(500ms)
std::copy
sse
axv
salida:
SSE: 1.11753x faster than std::copy
AVX: 1.81342x faster than std::copy
Entonces, en este caso, AVX es mucho más rápido que std::copy
. ¿Qué pasa cuando cambio al caso de prueba para ...
for blah blah:
sleep(500ms)
sse
axv
std::copy
Tenga en cuenta que absolutamente nada cambió, excepto el orden de las pruebas.
SSE: 0.797673x faster than std::copy
AVX: 0.809399x faster than std::copy
Woah ¿Cómo es eso posible? La CPU tarda un tiempo en alcanzar la velocidad máxima, por lo que las pruebas que se ejecutan más tarde tienen una ventaja. Esta pregunta tiene 3 respuestas ahora, incluida una respuesta ''aceptada''. Pero solo el que tenía la menor cantidad de upvotes estaba en el camino correcto.
Esta es una de las razones por las que la evaluación comparativa es difícil y nunca debe confiar en los micro-puntos de referencia de nadie a menos que hayan incluido información detallada de su configuración. No solo el código puede salir mal. Las características de ahorro de energía y los controladores extraños pueden desordenar completamente su punto de referencia. Una vez, he medido una diferencia de factor 7 en el rendimiento al presionar un interruptor en el BIOS que ofrece menos del 1% de los portátiles.
Escribir SSE rápido no es tan simple como usar operaciones SSE en lugar de sus equivalentes no paralelos. En este caso, sospecho que su compilador no puede desenrollar útilmente el par de carga / almacenamiento y su tiempo está dominado por los bloqueos causados por el uso de la salida de una operación de bajo rendimiento (la carga) en la siguiente instrucción (el almacén).
Puedes probar esta idea desenrollando manualmente una muesca:
//SSE-copy testing
start2 = std::chrono::system_clock::now();
for(int i=0; i<nn; ++i)
{
auto _mas = mas;
auto _tar = tar;
for(; _mas!=mas+sz; _mas+=8, _tar+=8)
{
__m128 buffer1 = _mm_load_ps(_mas);
__m128 buffer2 = _mm_load_ps(_mas+4);
_mm_store_ps(_tar, buffer1);
_mm_store_ps(_tar+4, buffer2);
}
}
Normalmente, cuando uso intrínsecos, desarmo la salida y me aseguro de que no esté ocurriendo ninguna locura (podría intentar esto para verificar si / cómo se desenrolló el bucle original). Para bucles más complejos, la herramienta correcta para usar es el Analizador de código de arquitectura Intel (IACA) . Es una herramienta de análisis estático que puede indicarle cosas como "tiene paradas en la tubería".
Esta es una pregunta muy interesante, pero creo que ninguna de las respuestas hasta ahora es correcta porque la pregunta en sí misma es tan engañosa.
El título debe cambiarse a "¿Cómo se alcanza el ancho de banda de E / S de la memoria teórica?"
Independientemente del conjunto de instrucciones que se utilice, la CPU es mucho más rápida que la RAM que la copia de memoria de bloque pura está limitada al 100% de E / S. Y esto explica por qué hay poca diferencia entre el rendimiento de SSE y AVX.
Para los búferes pequeños en caché L1D, AVX puede copiar significativamente más rápido que SSE en CPU como Haswell, donde las cargas / tiendas de 256b realmente utilizan una ruta de datos de 256b a la memoria caché L1D en lugar de dividirse en dos operaciones de 128b.
¡Irónicamente, el antiguo representante de instrucciones X86 funciona mucho mejor que SSE y AVX en términos de copia de memoria!
El artículo aquí explica cómo saturar el ancho de banda de la memoria realmente bien y tiene ricas referencias para explorar aún más.
Vea también REP MOVSB mejorado para memcpy aquí en SO, donde la respuesta de @ BeeOnRope analiza las tiendas NT (y las tiendas que no son RFO hechas por rep stosb/stosq
) versus las tiendas regulares, y cómo el ancho de banda de memoria de un solo núcleo a menudo está limitado por la máxima concurrencia / Latencia, no por el propio controlador de memoria.