performance - premium - ¿Cómo puedo comparar con precisión la velocidad de acceso sin alinear en x86_64
z3 plus vs z5 premium (3)
Al probar cargas de 64 bits para varias compensaciones (código a continuación), mis resultados sin procesar en Haswell son:
aligned L: 4.01115 T: 0.500003
ofs1 L: 4.00919 T: 0.500003
ofs2 L: 4.01494 T: 0.500003
ofs3 L: 4.01403 T: 0.500003
ofs7 L: 4.01073 T: 0.500003
ofs15 L: 4.01937 T: 0.500003
ofs31 L: 4.02107 T: 0.500002
ofs60 L: 9.01482 T: 1
ofs62 L: 9.03644 T: 1
ofs4092 L: 32.3014 T: 31.1967
aplique el redondeo como mejor le parezca, la mayoría de ellos obviamente deben redondearse hacia abajo, pero .3 y .2 (desde el cruce del límite de la página) son quizás demasiado significativos para ser ruido. Esto solo probó cargas con direcciones simples, y solo "cargas puras", sin reenvío.
Concluyo que la alineación dentro de una línea de caché no es relevante para las cargas escalares, solo cruza los límites de la línea de caché y (especialmente, y por razones obvias) es importante cruzar los límites de la página. Parece que no hay diferencia entre cruzar un límite de línea de caché exactamente en el medio o en otro lugar en este caso.
AMD ocasionalmente tiene algunos efectos divertidos con límites de 16 bytes, pero no puedo probar eso.
Y aquí están los resultados crudos (!) Del vector xmm que incluyen los efectos de
pextrq
, así que reste 2 ciclos de latencia:
aligned L: 8.05247 T: 0.500003
ofs1 L: 8.03223 T: 0.500003
ofs2 L: 8.02899 T: 0.500003
ofs3 L: 8.05598 T: 0.500003
ofs7 L: 8.03579 T: 0.500002
ofs15 L: 8.02787 T: 0.500003
ofs31 L: 8.05002 T: 0.500003
ofs58 L: 13.0404 T: 1
ofs60 L: 13.0825 T: 1
ofs62 L: 13.0935 T: 1
ofs4092 L: 36.345 T: 31.2357
El código de prueba fue
global test_unaligned_l
proc_frame test_unaligned_l
alloc_stack 8
[endprolog]
mov r9, rcx
rdtscp
mov r8d, eax
mov ecx, -10000000
mov rdx, r9
.loop:
mov rdx, [rdx]
mov rdx, [rdx]
add ecx, 1
jnc .loop
rdtscp
sub eax, r8d
add rsp, 8
ret
endproc_frame
global test_unaligned_tp
proc_frame test_unaligned_tp
alloc_stack 8
[endprolog]
mov r9, rcx
rdtscp
mov r8d, eax
mov ecx, -10000000
mov rdx, r9
.loop:
mov rax, [rdx]
mov rax, [rdx]
add ecx, 1
jnc .loop
rdtscp
sub eax, r8d
add rsp, 8
ret
endproc_frame
Para vectores muy similares pero con
pextrq
en la prueba de latencia.
Con algunos datos preparados en varias compensaciones, por ejemplo:
align 64
%rep 31
db 0
%endrep
unaligned31: dq unaligned31
align 4096
%rep 60
db 0
%endrep
unaligned60: dq unaligned60
align 4096
%rep 4092
db 0
%endrep
unaligned4092: dq unaligned4092
Para centrarme un poco más en el nuevo título, describiré lo que está tratando de hacer y por qué.
En primer lugar, hay una prueba de latencia.
Cargar un millón de cosas en
eax
desde un puntero que no está en
eax
(como lo hace el código en la pregunta) prueba el rendimiento, que es solo la mitad de la imagen.
Para cargas escalares que es trivial, para cargas vectoriales utilicé pares de:
movdqu xmm0, [rdx]
pextrq rdx, xmm0, 0
La latencia de
pextrq
es 2, es por eso que las cifras de latencia para las cargas vectoriales son 2 demasiado altas como se indicó.
Para facilitar esta prueba de latencia, los datos son un puntero autorreferencial. Es un escenario bastante atípico, pero no debería afectar las características de tiempo de las cargas.
La prueba de rendimiento tiene dos cargas por bucle en lugar de una para evitar que se produzca un cuello de botella por la sobrecarga del bucle. Se podrían utilizar más cargas, pero eso no es necesario en Haswell (o cualquier cosa que se me ocurra, pero en teoría podría existir un µarch con un rendimiento de rama inferior o un rendimiento de carga más alto).
No tengo mucho cuidado con las cercas en la lectura del TSC o la compensación de su sobrecarga (u otra sobrecarga). Tampoco desactivé Turbo, simplemente lo dejé correr a frecuencia turbo y dividido por la relación entre la tasa de TSC y la frecuencia turbo, lo que podría afectar un poco los tiempos. Todos estos efectos son muy pequeños en comparación con un punto de referencia del orden de 1E7, y los resultados se pueden redondear de todos modos.
Todos los tiempos fueron mejores de 30, cosas como el promedio y la varianza no tienen sentido en estos micro puntos de referencia, ya que la verdad fundamental no es un proceso aleatorio con parámetros que queremos estimar, sino algún entero fijo [1] (o múltiplo entero de un fracción, para el rendimiento). Casi todo el ruido es positivo, excepto el caso (relativamente teórico) de instrucciones de la "filtración" de referencia frente a la primera lectura de TSC (esto podría incluso evitarse si es necesario), por lo que es apropiado tomar el mínimo.
Nota 1: excepto al cruzar un límite de 4k aparentemente, algo extraño está sucediendo allí.
En una answer , he declarado que el acceso no alineado tiene casi la misma velocidad que el acceso alineado durante mucho tiempo (en x86 / x86_64). No tenía ningún número para respaldar esta declaración, así que he creado un punto de referencia para ello.
¿Ves alguna falla en este punto de referencia? ¿Puedes mejorarlo (quiero decir, aumentar GB / seg, para que refleje mejor la verdad)?
#include <sys/time.h>
#include <stdio.h>
template <int N>
__attribute__((noinline))
void loop32(const char *v) {
for (int i=0; i<N; i+=160) {
__asm__ ("mov (%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x04(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x08(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x0c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x10(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x14(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x18(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x1c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x20(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x24(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x28(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x2c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x30(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x34(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x38(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x3c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x40(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x44(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x48(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x4c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x50(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x54(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x58(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x5c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x60(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x64(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x68(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x6c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x70(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x74(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x78(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x7c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x80(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x84(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x88(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x8c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x90(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x94(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x98(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x9c(%0), %%eax" : : "r"(v) :"eax");
v += 160;
}
}
template <int N>
__attribute__((noinline))
void loop64(const char *v) {
for (int i=0; i<N; i+=160) {
__asm__ ("mov (%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x08(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x10(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x18(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x20(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x28(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x30(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x38(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x40(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x48(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x50(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x58(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x60(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x68(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x70(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x78(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x80(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x88(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x90(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x98(%0), %%rax" : : "r"(v) :"rax");
v += 160;
}
}
template <int N>
__attribute__((noinline))
void loop128a(const char *v) {
for (int i=0; i<N; i+=160) {
__asm__ ("movaps (%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x10(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x20(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x30(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x40(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x50(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x60(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x70(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x80(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x90(%0), %%xmm0" : : "r"(v) :"xmm0");
v += 160;
}
}
template <int N>
__attribute__((noinline))
void loop128u(const char *v) {
for (int i=0; i<N; i+=160) {
__asm__ ("movups (%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x10(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x20(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x30(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x40(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x50(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x60(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x70(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x80(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x90(%0), %%xmm0" : : "r"(v) :"xmm0");
v += 160;
}
}
long long int t() {
struct timeval tv;
gettimeofday(&tv, 0);
return (long long int)tv.tv_sec*1000000 + tv.tv_usec;
}
int main() {
const int ITER = 10;
const int N = 1600000000;
char *data = reinterpret_cast<char *>(((reinterpret_cast<unsigned long long>(new char[N+32])+15)&~15));
for (int i=0; i<N+16; i++) data[i] = 0;
{
long long int t0 = t();
for (int i=0; i<ITER*100000; i++) {
loop32<N/100000>(data);
}
long long int t1 = t();
for (int i=0; i<ITER*100000; i++) {
loop32<N/100000>(data+1);
}
long long int t2 = t();
for (int i=0; i<ITER; i++) {
loop32<N>(data);
}
long long int t3 = t();
for (int i=0; i<ITER; i++) {
loop32<N>(data+1);
}
long long int t4 = t();
printf(" 32-bit, cache: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3f%%/n", (double)N*ITER/(t1-t0)/1000, (double)N*ITER/(t2-t1)/1000, 100.0*(t2-t1)/(t1-t0)-100.0f);
printf(" 32-bit, mem: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3f%%/n", (double)N*ITER/(t3-t2)/1000, (double)N*ITER/(t4-t3)/1000, 100.0*(t4-t3)/(t3-t2)-100.0f);
}
{
long long int t0 = t();
for (int i=0; i<ITER*100000; i++) {
loop64<N/100000>(data);
}
long long int t1 = t();
for (int i=0; i<ITER*100000; i++) {
loop64<N/100000>(data+1);
}
long long int t2 = t();
for (int i=0; i<ITER; i++) {
loop64<N>(data);
}
long long int t3 = t();
for (int i=0; i<ITER; i++) {
loop64<N>(data+1);
}
long long int t4 = t();
printf(" 64-bit, cache: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3f%%/n", (double)N*ITER/(t1-t0)/1000, (double)N*ITER/(t2-t1)/1000, 100.0*(t2-t1)/(t1-t0)-100.0f);
printf(" 64-bit, mem: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3f%%/n", (double)N*ITER/(t3-t2)/1000, (double)N*ITER/(t4-t3)/1000, 100.0*(t4-t3)/(t3-t2)-100.0f);
}
{
long long int t0 = t();
for (int i=0; i<ITER*100000; i++) {
loop128a<N/100000>(data);
}
long long int t1 = t();
for (int i=0; i<ITER*100000; i++) {
loop128u<N/100000>(data+1);
}
long long int t2 = t();
for (int i=0; i<ITER; i++) {
loop128a<N>(data);
}
long long int t3 = t();
for (int i=0; i<ITER; i++) {
loop128u<N>(data+1);
}
long long int t4 = t();
printf("128-bit, cache: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3f%%/n", (double)N*ITER/(t1-t0)/1000, (double)N*ITER/(t2-t1)/1000, 100.0*(t2-t1)/(t1-t0)-100.0f);
printf("128-bit, mem: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3f%%/n", (double)N*ITER/(t3-t2)/1000, (double)N*ITER/(t4-t3)/1000, 100.0*(t4-t3)/(t3-t2)-100.0f);
}
}
Estoy poniendo mi punto de referencia un poco mejorado aquí. Todavía mide solo el rendimiento (y solo el desplazamiento 1 no alineado). En base a las otras respuestas, agregué divisiones de 64 y 4096 bytes.
¡Para divisiones de 4k, hay una gran diferencia! Pero si los datos no cruzan el límite de 64 bytes, no hay pérdida de velocidad (al menos para estos 2 procesadores que he probado).
Al observar estos números (y los números en otras respuestas), mi conclusión es que el acceso no alineado es rápido en promedio (tanto el rendimiento como la latencia), pero hay casos en que puede ser mucho más lento. Pero esto no significa que se desaconseje su uso.
Los números brutos producidos por mi punto de referencia deben tomarse con un grano de sal (es muy probable que un código ASM correctamente escrito lo supere), pero estos resultados coinciden principalmente con la respuesta de Harold para Haswell (columna de diferencia).
Haswell:
Full:
32-bit, cache: aligned: 33.2901 GB/sec unaligned: 29.5063 GB/sec, difference: 1.128x
32-bit, mem: aligned: 12.1597 GB/sec unaligned: 12.0659 GB/sec, difference: 1.008x
64-bit, cache: aligned: 66.0368 GB/sec unaligned: 52.8914 GB/sec, difference: 1.249x
64-bit, mem: aligned: 16.1317 GB/sec unaligned: 16.0568 GB/sec, difference: 1.005x
128-bit, cache: aligned: 129.8730 GB/sec unaligned: 87.9791 GB/sec, difference: 1.476x
128-bit, mem: aligned: 16.8150 GB/sec unaligned: 16.8151 GB/sec, difference: 1.000x
JustBoundary64:
32-bit, cache: aligned: 32.5555 GB/sec unaligned: 16.0175 GB/sec, difference: 2.032x
32-bit, mem: aligned: 1.0044 GB/sec unaligned: 1.0001 GB/sec, difference: 1.004x
64-bit, cache: aligned: 65.2707 GB/sec unaligned: 32.0431 GB/sec, difference: 2.037x
64-bit, mem: aligned: 2.0093 GB/sec unaligned: 2.0007 GB/sec, difference: 1.004x
128-bit, cache: aligned: 130.6789 GB/sec unaligned: 64.0851 GB/sec, difference: 2.039x
128-bit, mem: aligned: 4.0180 GB/sec unaligned: 3.9994 GB/sec, difference: 1.005x
WithoutBoundary64:
32-bit, cache: aligned: 33.2911 GB/sec unaligned: 33.2916 GB/sec, difference: 1.000x
32-bit, mem: aligned: 11.6156 GB/sec unaligned: 11.6223 GB/sec, difference: 0.999x
64-bit, cache: aligned: 65.9117 GB/sec unaligned: 65.9548 GB/sec, difference: 0.999x
64-bit, mem: aligned: 14.3200 GB/sec unaligned: 14.3027 GB/sec, difference: 1.001x
128-bit, cache: aligned: 128.2605 GB/sec unaligned: 128.3342 GB/sec, difference: 0.999x
128-bit, mem: aligned: 12.6352 GB/sec unaligned: 12.6218 GB/sec, difference: 1.001x
JustBoundary4096:
32-bit, cache: aligned: 33.5500 GB/sec unaligned: 0.5415 GB/sec, difference: 61.953x
32-bit, mem: aligned: 0.4527 GB/sec unaligned: 0.0431 GB/sec, difference: 10.515x
64-bit, cache: aligned: 67.1141 GB/sec unaligned: 1.0836 GB/sec, difference: 61.937x
64-bit, mem: aligned: 0.9112 GB/sec unaligned: 0.0861 GB/sec, difference: 10.582x
128-bit, cache: aligned: 134.2000 GB/sec unaligned: 2.1668 GB/sec, difference: 61.936x
128-bit, mem: aligned: 1.8165 GB/sec unaligned: 0.1700 GB/sec, difference: 10.687x
Sandy Bridge (processor from 2011)
Full:
32-bit, cache: aligned: 30.0302 GB/sec unaligned: 26.2587 GB/sec, difference: 1.144x
32-bit, mem: aligned: 11.0317 GB/sec unaligned: 10.9358 GB/sec, difference: 1.009x
64-bit, cache: aligned: 59.2220 GB/sec unaligned: 41.5515 GB/sec, difference: 1.425x
64-bit, mem: aligned: 14.5985 GB/sec unaligned: 14.3760 GB/sec, difference: 1.015x
128-bit, cache: aligned: 115.7643 GB/sec unaligned: 45.0905 GB/sec, difference: 2.567x
128-bit, mem: aligned: 14.8561 GB/sec unaligned: 14.8220 GB/sec, difference: 1.002x
JustBoundary64:
32-bit, cache: aligned: 15.2127 GB/sec unaligned: 3.1037 GB/sec, difference: 4.902x
32-bit, mem: aligned: 0.9870 GB/sec unaligned: 0.6110 GB/sec, difference: 1.615x
64-bit, cache: aligned: 30.2074 GB/sec unaligned: 6.2258 GB/sec, difference: 4.852x
64-bit, mem: aligned: 1.9739 GB/sec unaligned: 1.2194 GB/sec, difference: 1.619x
128-bit, cache: aligned: 60.7265 GB/sec unaligned: 12.4007 GB/sec, difference: 4.897x
128-bit, mem: aligned: 3.9443 GB/sec unaligned: 2.4460 GB/sec, difference: 1.613x
WithoutBoundary64:
32-bit, cache: aligned: 30.0348 GB/sec unaligned: 29.9801 GB/sec, difference: 1.002x
32-bit, mem: aligned: 10.7067 GB/sec unaligned: 10.6755 GB/sec, difference: 1.003x
64-bit, cache: aligned: 59.1895 GB/sec unaligned: 59.1925 GB/sec, difference: 1.000x
64-bit, mem: aligned: 12.9404 GB/sec unaligned: 12.9307 GB/sec, difference: 1.001x
128-bit, cache: aligned: 116.4629 GB/sec unaligned: 116.0778 GB/sec, difference: 1.003x
128-bit, mem: aligned: 11.2963 GB/sec unaligned: 11.3533 GB/sec, difference: 0.995x
JustBoundary4096:
32-bit, cache: aligned: 30.2457 GB/sec unaligned: 0.5626 GB/sec, difference: 53.760x
32-bit, mem: aligned: 0.4055 GB/sec unaligned: 0.0275 GB/sec, difference: 14.726x
64-bit, cache: aligned: 60.6175 GB/sec unaligned: 1.1257 GB/sec, difference: 53.851x
64-bit, mem: aligned: 0.8150 GB/sec unaligned: 0.0551 GB/sec, difference: 14.798x
128-bit, cache: aligned: 121.2121 GB/sec unaligned: 2.2455 GB/sec, difference: 53.979x
128-bit, mem: aligned: 1.6255 GB/sec unaligned: 0.1103 GB/sec, difference: 14.744x
Aquí está el código:
#include <sys/time.h>
#include <stdio.h>
__attribute__((always_inline))
void load32(const char *v) {
__asm__ ("mov %0, %%eax" : : "m"(*v) :"eax");
}
__attribute__((always_inline))
void load64(const char *v) {
__asm__ ("mov %0, %%rax" : : "m"(*v) :"rax");
}
__attribute__((always_inline))
void load128a(const char *v) {
__asm__ ("movaps %0, %%xmm0" : : "m"(*v) :"xmm0");
}
__attribute__((always_inline))
void load128u(const char *v) {
__asm__ ("movups %0, %%xmm0" : : "m"(*v) :"xmm0");
}
struct Full {
template <int S>
static float factor() {
return 1.0f;
}
template <void (*LOAD)(const char *), int S, int N>
static void loop(const char *v) {
for (int i=0; i<N; i+=S*16) {
LOAD(v+S* 0);
LOAD(v+S* 1);
LOAD(v+S* 2);
LOAD(v+S* 3);
LOAD(v+S* 4);
LOAD(v+S* 5);
LOAD(v+S* 6);
LOAD(v+S* 7);
LOAD(v+S* 8);
LOAD(v+S* 9);
LOAD(v+S*10);
LOAD(v+S*11);
LOAD(v+S*12);
LOAD(v+S*13);
LOAD(v+S*14);
LOAD(v+S*15);
v += S*16;
}
}
};
struct JustBoundary64 {
template <int S>
static float factor() {
return S/64.0f;
}
template <void (*LOAD)(const char *), int S, int N>
static void loop(const char *v) {
static_assert(N%(64*16)==0);
for (int i=0; i<N; i+=64*16) {
LOAD(v+64* 1-S);
LOAD(v+64* 2-S);
LOAD(v+64* 3-S);
LOAD(v+64* 4-S);
LOAD(v+64* 5-S);
LOAD(v+64* 6-S);
LOAD(v+64* 7-S);
LOAD(v+64* 8-S);
LOAD(v+64* 9-S);
LOAD(v+64*10-S);
LOAD(v+64*11-S);
LOAD(v+64*12-S);
LOAD(v+64*13-S);
LOAD(v+64*14-S);
LOAD(v+64*15-S);
LOAD(v+64*16-S);
v += 64*16;
}
}
};
struct WithoutBoundary64 {
template <int S>
static float factor() {
return (64-S)/64.0f;
}
template <void (*LOAD)(const char *), int S, int N>
static void loop(const char *v) {
for (int i=0; i<N; i+=S*16) {
if ((S* 1)&0x3f) LOAD(v+S* 0);
if ((S* 2)&0x3f) LOAD(v+S* 1);
if ((S* 3)&0x3f) LOAD(v+S* 2);
if ((S* 4)&0x3f) LOAD(v+S* 3);
if ((S* 5)&0x3f) LOAD(v+S* 4);
if ((S* 6)&0x3f) LOAD(v+S* 5);
if ((S* 7)&0x3f) LOAD(v+S* 6);
if ((S* 8)&0x3f) LOAD(v+S* 7);
if ((S* 9)&0x3f) LOAD(v+S* 8);
if ((S*10)&0x3f) LOAD(v+S* 9);
if ((S*11)&0x3f) LOAD(v+S*10);
if ((S*12)&0x3f) LOAD(v+S*11);
if ((S*13)&0x3f) LOAD(v+S*12);
if ((S*14)&0x3f) LOAD(v+S*13);
if ((S*15)&0x3f) LOAD(v+S*14);
if ((S*16)&0x3f) LOAD(v+S*15);
v += S*16;
}
}
};
struct JustBoundary4096 {
template <int S>
static float factor() {
return S/4096.0f;
}
template <void (*LOAD)(const char *), int S, int N>
static void loop(const char *v) {
static_assert(N%(4096*4)==0);
for (int i=0; i<N; i+=4096*4) {
LOAD(v+4096*1-S);
LOAD(v+4096*2-S);
LOAD(v+4096*3-S);
LOAD(v+4096*4-S);
v += 4096*4;
}
}
};
long long int t() {
struct timeval tv;
gettimeofday(&tv, 0);
return (long long int)tv.tv_sec*1000000 + tv.tv_usec;
}
template <typename TYPE, void (*LOADa)(const char *), void (*LOADu)(const char *), int S, int N>
void bench(const char *data, int iter, const char *name) {
long long int t0 = t();
for (int i=0; i<iter*100000; i++) {
TYPE::template loop<LOADa, S, N/100000>(data);
}
long long int t1 = t();
for (int i=0; i<iter*100000; i++) {
TYPE::template loop<LOADu, S, N/100000>(data+1);
}
long long int t2 = t();
for (int i=0; i<iter; i++) {
TYPE::template loop<LOADa, S, N>(data);
}
long long int t3 = t();
for (int i=0; i<iter; i++) {
TYPE::template loop<LOADu, S, N>(data+1);
}
long long int t4 = t();
printf("%s-bit, cache: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3fx/n", name, (double)N*iter/(t1-t0)/1000*TYPE::template factor<S>(), (double)N*iter/(t2-t1)/1000*TYPE::template factor<S>(), (float)(t2-t1)/(t1-t0));
printf("%s-bit, mem: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3fx/n", name, (double)N*iter/(t3-t2)/1000*TYPE::template factor<S>(), (double)N*iter/(t4-t3)/1000*TYPE::template factor<S>(), (float)(t4-t3)/(t3-t2));
}
int main() {
const int ITER = 10;
const int N = 1638400000;
char *data = reinterpret_cast<char *>(((reinterpret_cast<unsigned long long>(new char[N+8192])+4095)&~4095));
for (int i=0; i<N+8192; i++) data[i] = 0;
printf("Full:/n");
bench<Full, load32, load32, 4, N>(data, ITER, " 32");
bench<Full, load64, load64, 8, N>(data, ITER, " 64");
bench<Full, load128a, load128u, 16, N>(data, ITER, "128");
printf("/nJustBoundary64:/n");
bench<JustBoundary64, load32, load32, 4, N>(data, ITER, " 32");
bench<JustBoundary64, load64, load64, 8, N>(data, ITER, " 64");
bench<JustBoundary64, load128a, load128u, 16, N>(data, ITER, "128");
printf("/nWithoutBoundary64:/n");
bench<WithoutBoundary64, load32, load32, 4, N>(data, ITER, " 32");
bench<WithoutBoundary64, load64, load64, 8, N>(data, ITER, " 64");
bench<WithoutBoundary64, load128a, load128u, 16, N>(data, ITER, "128");
printf("/nJustBoundary4096:/n");
bench<JustBoundary4096, load32, load32, 4, N>(data, ITER*10, " 32");
bench<JustBoundary4096, load64, load64, 8, N>(data, ITER*10, " 64");
bench<JustBoundary4096, load128a, load128u, 16, N>(data, ITER*10, "128");
}
Método de tiempo
Probablemente lo habría configurado para que la prueba fuera seleccionada por un
perf stat ./unaligned-test
línea de comando, para poder
perf stat ./unaligned-test
con
perf stat ./unaligned-test
y obtener resultados de contador de rendimiento en lugar de solo tiempos de reloj de pared para cada prueba.
De esa manera, no tendría que preocuparme por el turbo / ahorro de energía, ya que podría medir en ciclos de reloj de núcleo.
(No es lo mismo que los ciclos de referencia
rdtsc
/
rdtsc
menos que desactive el turbo y otras variaciones de frecuencia).
Solo está probando el rendimiento, no la latencia, porque ninguna de las cargas depende.
Sus números de caché serán peores que sus números de memoria, pero tal vez no se dé cuenta de que es porque sus números de caché pueden deberse a un cuello de botella en el número de registros de carga dividida que manejan cargas / almacenes que cruzan un límite de línea de caché. Para la lectura secuencial, los niveles externos de caché siempre verán una secuencia de solicitudes de líneas de caché completas. Solo las unidades de ejecución que obtienen datos de L1D tienen que preocuparse por la alineación. Para probar la desalineación para el caso no almacenado en caché, puede hacer cargas dispersas, por lo que las divisiones de línea de caché necesitarían traer dos líneas de caché a L1.
Las líneas de caché tienen 64 B de ancho 1 , por lo que siempre está probando una combinación de divisiones de línea de caché y accesos dentro de una línea de caché. Probar cargas siempre divididas supondría un cuello de botella más difícil en los recursos de microarquitectura de carga dividida. (En realidad, dependiendo de su CPU, el ancho de búsqueda de caché puede ser más estrecho que el tamaño de la línea . Las CPU Intel recientes pueden buscar cualquier fragmento no alineado desde el interior de una línea de caché, pero eso es porque tienen un hardware especial para hacerlo tan rápido. Otras CPU pueden solo sea lo más rápido posible al buscar dentro de un trozo de 16B alineado naturalmente o algo así. @BeeOnRope dice que las CPU AMD pueden preocuparse por los límites de 16B y 32B ).
No estás probando store-> load forwarding en absoluto. Para ver las pruebas existentes, y una buena manera de visualizar los resultados para diferentes alineaciones, vea esta publicación del blog stuffedcow.net: Reenvío de almacenamiento a carga y desambiguación de memoria en procesadores x86 .
Pasar datos a través de la memoria es un caso de uso importante, y la desalineación + divisiones de línea de caché pueden interferir con el reenvío de la tienda en algunas CPU. Para probar esto correctamente, asegúrese de probar diferentes desalineaciones, no solo 1:15 (vector) o 1: 3 (entero). (Actualmente, solo prueba un desplazamiento de +1 en relación con la alineación 16B).
Olvidé si es solo para el reenvío de la tienda, o para cargas regulares, pero puede haber menos penalización cuando una carga se divide de manera uniforme en un límite de línea de caché (un vector 8: 8, y tal vez también 4: 4 o 2: 2 divisiones enteras).
Deberías probar esto.
(Podría estar pensando en P4
lddqu
o Core 2
movqdu
)
El manual de optimización de Intel tiene grandes tablas de desalineación frente al reenvío de tiendas de una tienda amplia a recargas estrechas que están completamente contenidas. En algunas CPU, esto funciona en más casos cuando la tienda amplia estaba alineada de forma natural, incluso si no cruza los límites de la línea de caché. (Tal vez en SnB / IvB, ya que usan un caché L1 almacenado con bancos de 16B, y las divisiones entre ellos pueden afectar el reenvío de la tienda. No volví a revisar el manual, pero si realmente quieres probar esto experimentalmente, eso es algo que tú debería estar buscando)
Lo que me recuerda, es más probable que las cargas desalineadas provoquen conflictos entre bancos de caché en SnB / IvB (porque una carga puede tocar dos bancos). Pero no verá esta carga desde una sola transmisión, porque acceder al mismo banco en la misma línea dos veces en un ciclo está bien. Solo está accediendo al mismo banco en diferentes líneas que no pueden suceder en el mismo ciclo. (p. ej., cuando dos accesos de memoria están separados por un múltiplo de 128B).
No intentes probar 4k divisiones de página. Son más lentos que las divisiones regulares de la línea de caché, porque también necesitan dos comprobaciones de TLB. (Sin embargo, Skylake los mejoró de ~ 100 penalizaciones de ciclo a ~ 5 penalizaciones de ciclo más allá de la latencia normal de uso de carga)
No puede probar
movups
en direcciones alineadas
, por lo que no detectaría que
movups
es más lento que
movaps
en Core2 y anteriores incluso cuando la memoria está alineada en tiempo de ejecución.
(Creo que las cargas
mov
no alineadas de hasta 8 bytes estaban bien incluso en Core2, siempre y cuando no cruzaran un límite de línea de caché. IDK cuántos años tendría que mirar una CPU para encontrar un problema con un no vector cargas dentro de una línea de caché. Sería una CPU de solo 32 bits, pero aún podría probar cargas 8B con MMX o SSE, o incluso x87. P5 Pentium y luego garantizar que las cargas / tiendas alineadas de 8B son atómicas, pero P6 y más nuevas garantiza que las cargas / tiendas 8B almacenadas en caché son atómicas siempre que no se crucen los límites de la línea de caché. A diferencia de AMD, donde los límites de 8B son importantes para las garantías de atomicidad incluso en la memoria almacenable en caché.
Mire las cosas de Agner Fog para obtener más información sobre cómo las cargas no alineadas pueden ser más lentas, y prepare pruebas para ejercitar esos casos. En realidad, Agner puede no ser el mejor recurso para eso, ya que su guía de microarquitectura se enfoca principalmente en ayudar a superar los problemas. Solo una breve mención del costo de las divisiones de línea de caché, nada en profundidad sobre el rendimiento frente a la latencia.
Ver también: Cacheline se divide, toma dos , del blog de Dark Shikari (desarrollador líder x264), hablando de estrategias de carga no alineadas en Core2: valió la pena verificar la alineación y usar una estrategia diferente para el bloque.
Notas al pie:
- 64B líneas de caché es una suposición segura en estos días. Pentium 3 y anteriores tenían líneas 32B. P4 tenía líneas 64B pero a menudo se transfirieron en pares alineados con 128B. Pensé que recordaba haber leído que P4 en realidad tenía líneas 128B en L2 o L3, pero tal vez eso era solo una distorsión de las líneas 64B transferidas en pares. 7-CPU definitivamente dice 64B líneas en ambos niveles de caché para un P4 130nm .
Ver también uarch-bench para Skylake . Aparentemente, alguien ya ha escrito un probador que verifica cada posible desalineación relativa a un límite de línea de caché.
Mis pruebas en el escritorio de Skylake (i7-6700k):
El modo de direccionamiento afecta la latencia de uso de carga, exactamente como los documentos de Intel en su manual de optimización.
mov rax, [rax+...]
con el entero
mov rax, [rax+...]
y con
movzx/sx
(en ese caso usando el valor cargado como índice, ya que es demasiado estrecho para ser un puntero).
;;; Linux x86-64 NASM/YASM source. Assemble into a static binary
;; public domain, originally written by [email protected].
;; Share and enjoy. If it breaks, you get to keep both pieces.
;;; This kind of grew while I was testing and thinking of things to test
;;; I left in some of the comments, but took out most of them and summarized the results outside this code block
;;; When I thought of something new to test, I''d edit, save, and up-arrow my assemble-and-run shell command
;;; Then edit the result into a comment in the source.
section .bss
ALIGN 2 * 1<<20 ; 2MB = 4096*512. Uses hugepages in .bss but not in .data. I checked in /proc/<pid>/smaps
buf: resb 16 * 1<<20
section .text
global _start
_start:
mov esi, 128
; mov edx, 64*123 + 8
; mov edx, 64*123 + 0
; mov edx, 64*64 + 0
xor edx,edx
;; RAX points into buf, 16B into the last 4k page of a 2M hugepage
mov eax, buf + (2<<20)*0 + 4096*511 + 64*0 + 16
mov ecx, 25000000
%define ADDR(x) x ; SKL: 4c
;%define ADDR(x) x + rdx ; SKL: 5c
;%define ADDR(x) 128+60 + x + rdx*2 ; SKL: 11c cache-line split
;%define ADDR(x) x-8 ; SKL: 5c
;%define ADDR(x) x-7 ; SKL: 12c for 4k-split (even if it''s in the middle of a hugepage)
; ... many more things and a block of other result-recording comments taken out
%define dst rax
mov [ADDR(rax)], dst
align 32
.loop:
mov dst, [ADDR(rax)]
mov dst, [ADDR(rax)]
mov dst, [ADDR(rax)]
mov dst, [ADDR(rax)]
dec ecx
jnz .loop
xor edi,edi
mov eax,231
syscall
Luego corre con
asm-link load-use-latency.asm && disas load-use-latency &&
perf stat -etask-clock,cycles,L1-dcache-loads,instructions,branches -r4 ./load-use-latency
+ yasm -felf64 -Worphan-labels -gdwarf2 load-use-latency.asm
+ ld -o load-use-latency load-use-latency.o
(disassembly output so my terminal history has the asm with the perf results)
Performance counter stats for ''./load-use-latency'' (4 runs):
91.422838 task-clock:u (msec) # 0.990 CPUs utilized ( +- 0.09% )
400,105,802 cycles:u # 4.376 GHz ( +- 0.00% )
100,000,013 L1-dcache-loads:u # 1093.819 M/sec ( +- 0.00% )
150,000,039 instructions:u # 0.37 insn per cycle ( +- 0.00% )
25,000,031 branches:u # 273.455 M/sec ( +- 0.00% )
0.092365514 seconds time elapsed ( +- 0.52% )
En este caso, estaba probando
mov rax, [rax]
, alineado naturalmente, por lo que ciclos = 4 * L1-dcache-cargas.
Latencia 4c.
No desactivé el turbo ni nada de eso.
Como nada sale del núcleo, los ciclos de reloj del núcleo son la mejor manera de medir.
-
[base + 0..2047]
: latencia de uso de carga de 4c, división de línea de caché de 11c, división de 4 páginas de 11c (incluso dentro de la misma página enorme). Ver ¿Hay una penalización cuando base + offset está en una página diferente a la base? para más detalles: sibase+disp
resulta estar en una página diferente a labase
, se debe reproducir la carga uop. -
cualquier otro modo de direccionamiento: latencia de 5c, división de línea de caché de 11c, división de 4c de 4c (incluso dentro de una página enorme).
Esto incluye
[rax - 16]
. No es disp8 vs disp32 lo que hace la diferencia.
Entonces: las grandes páginas no ayudan a evitar las penalizaciones por división de página (al menos no cuando ambas páginas están activas en el TLB). Una división de línea de caché hace que el modo de direccionamiento sea irrelevante, pero los modos de direccionamiento "rápido" tienen una latencia 1c menor para cargas normales y de división de página.
El manejo de división 4k es fantásticamente mejor que antes, vea los números de @ harold donde Haswell tiene una latencia de ~ 32c para una división 4k (Y las CPU más antiguas pueden ser incluso peores que eso. Pensé que antes de SKL se suponía que era una penalización de ~ 100 ciclos).
Rendimiento (independientemente del modo de direccionamiento)
, medido utilizando un destino distinto de
rax
para que las cargas sean independientes:
- sin división: 0.5c.
- División CL: 1c.
- 4k-split: ~ 3.8 a 3.9c ( mucho mejor que las CPUs anteriores a Skylake)
El mismo rendimiento / latencia para
movzx/movsx
(incluidas las divisiones de WORD), como se esperaba porque se manejan en el puerto de carga (a diferencia de algunas CPU AMD, donde también hay un UOP ALU).
Las cargas divididas de la línea de caché se reproducen desde la RS (estación de reserva).
contadores para
uops_dispatched_port.port_2
+
port_3
= 2x número de
mov rdi, [rdi]
, en otra prueba usando básicamente el mismo bucle.
(Este fue un caso de carga dependiente, no limitado en rendimiento). No puede detectar una carga dividida hasta después de AGU.
Presumiblemente, cuando una carga uop descubre que necesita datos de una segunda línea, busca un registro dividido (el búfer que usan las CPU de Intel para manejar cargas divididas), y coloca la parte necesaria de los datos de la primera línea en esa división reg. Y también le indica al RS que necesita reproducirse nuevamente. (Esto es conjeturas).
Consulte también Efectos de rendimiento extraños de tiendas dependientes cercanas en un bucle de búsqueda de puntero en IvyBridge. ¿Agregar una carga extra lo acelera? para más información sobre las repeticiones de uop. (Pero tenga en cuenta que es para uops que dependen de una carga, no la carga uop en sí. Creo que una carga perdida de caché configura todo para usar los datos cuando llega, sin tener que reproducirse de nuevo. El problema es que el programador programa agresivamente uops que consumen los datos para despachar en el ciclo cuando los datos de carga pueden llegar desde el caché L2, en lugar de esperar un ciclo adicional para ver si lo hizo o no. En esas preguntas y respuestas, los uops dependientes también son en su mayoría cargas).
Por lo tanto, creo que incluso si no hay una línea de caché presente, la repetición de carga dividida debe ocurrir dentro de unos pocos ciclos, por lo que las solicitudes de carga de demanda para ambos lados de la división pueden estar en vuelo a la vez.
SKL tiene dos unidades de paso de página de hardware, lo que probablemente esté relacionado con la mejora masiva en el rendimiento dividido en 4k . Incluso cuando no hay fallas de TLB, presumiblemente las CPU más antiguas tuvieron que tener en cuenta el hecho de que podría haberlas.
Es interesante que el rendimiento dividido en 4k no sea entero. Creo que mis mediciones tenían suficiente precisión y repetibilidad para decir esto. Recuerde que esto es con cada carga dividida en 4k, y ningún otro trabajo en curso (excepto estar dentro de un pequeño bucle dec / jnz). Si alguna vez tienes esto en código real, estás haciendo algo realmente mal.
No tengo conjeturas sólidas sobre por qué podría ser no entero, pero claramente hay muchas cosas que tienen que suceder microarquitecturalmente para una división de 4k. Todavía es una división de línea de caché, y tiene que verificar el TLB dos veces.