procesadores Intel puede emitir dos operaciones de punto flotante, pero una carga por ciclo, por lo que los accesos de memoria son la restricción más apretado.Con esto en mente, primero apunté a usar cargas empaquetadas para reducir el número de instrucciones de carga, y usé la aritmética compacta solo porque era conveniente. Desde entonces, me he dado cuenta de que saturar el ancho de banda de la memoria puede ser el mayor problema, y todo el juego con las instrucciones SSE podría haber sido una optimización prematura si el objetivo era hacer que el código fuera rápido en lugar de aprender a vectorizar.
SSE
Las cargas menor número posible con ninguna suposición sobre los índices en b
requiere desenrollar el bucle de cuatro veces. Una carga de 128 bits obtiene cuatro índices de b
, dos cargas de 128 bits cada una obtiene un par de dobles adyacentes desde c
, y la recopilación de a
requiere cargas independientes de 64 bits. Eso es un piso de 7 ciclos por cada cuatro iteraciones para el código de serie. (suficiente para saturar el ancho de banda de mi memoria si el acceso a a
no guarda en caché). He dejado de lado algunas cosas molestas como manejar un número de iteraciones que no es un múltiplo de 4.
entry: ; (rdi,rsi,rdx,rcx) are (n,a,b,c)
xorpd xmm0, xmm0
xor r8, r8
loop:
movdqa xmm1, [rdx+4*r8]
movapd xmm2, [rcx+8*r8]
movapd xmm3, [rcx+8*r8+8]
movd r9, xmm1
movq r10, xmm1
movsd xmm4, [rsi+8*r9]
shr r10, 32
movhpd xmm4, [rsi+8*r10]
punpckhqdq xmm1, xmm1
movd r9, xmm1
movq r10, xmm1
movsd xmm5, [rsi+8*r9]
shr r10, 32
movhpd xmm5, [rsi+8*r10]
add r8, 4
cmp r8, rdi
mulpd xmm2, xmm4
mulpd xmm3, xmm5
addpd xmm0, xmm2
addpd xmm0, xmm3
jl loop
Obtención de los índices a cabo es la parte más complicada. movdqa
carga 128 bits de datos enteros desde una dirección alineada de 16 bytes (Nehalem tiene penalizaciones de latencia para mezclar las instrucciones de SSE "entero" y "flotante"). punpckhqdq
mueve alto 64 bits a 64 bits bajo, pero en modo entero a diferencia del nombre más simple movhlpd
. Los cambios de 32 bits se realizan en los registros de propósito general. movhpd
carga una doble en la parte superior de un registro xmm sin molestar a la parte inferior; esto se usa para cargar los elementos de a
directamente en los registros empaquetados.
Este código es claramente más rápido que el código anterior, que a su vez es más rápido que el código simple, y en cada patrón de acceso, excepto en el caso simple B[i] = i
donde el lazo ingenuo es realmente el más rápido. También intenté algunas cosas como una función alrededor de SUM(A(B(:)),C(:))
en Fortran que terminó básicamente equivalente al bucle simple.
Probé en un Q6600 (65 nm Core 2 a 2.4Ghz) con 4 GB de memoria DDR2-667, en 4 módulos. Probar el ancho de banda de la memoria da unos 5333 MB/s, por lo que parece que solo veo un solo canal. Estoy compilando con gcc 4.3.2-1.1, -O3 -Fast-math -msse2 -Ftree-vectorize -std = gnu99 de Debian.
Para la prueba de que estoy dejando n
ser un millón, inicializar las matrices de manera a[b[i]]
y c[i]
ambos iguales 1.0/(i+1)
, con unos patrones diferentes de índices. Una asigna a
con un millón de elementos y conjuntos de b
a una permutación aleatoria, otro asigna a
con elementos 10M y utiliza cada décima, y los últimos asigna a
con elementos 10M y configura b[i+1]
mediante la adición de un número al azar de 1 a 9 para b[i]
. Estoy cronometrando cuánto tarda una llamada con gettimeofday
, borrando las cachés llamando al clflush
en las matrices y midiendo 1000 pruebas de cada función. Tracé distribuciones de tiempo de ejecución suavizadas usando algún código de las entrañas de criterion (en particular, el estimador de densidad de kernel en el paquete statistics
).
ancho de banda
Ahora, para la nota real importante acerca de ancho de banda. 5333MB/s con reloj de 2.4Ghz es un poco más de dos bytes por ciclo. Mis datos son lo suficientemente largos como para que nada pueda almacenarse en caché, y multiplicar el tiempo de ejecución de mi ciclo por (16 + 2 * 16 + 4 * 64) bytes cargados por iteración si todo falla me da casi exactamente el ancho de banda de ~ 5333MB/s que mi sistema tiene . Debería ser bastante fácil saturar ese ancho de banda sin SSE.Aun suponiendo a
se almacenan en caché por completo, sólo la lectura y b
c
para una iteración mueve 12 bytes de datos, y la ingenua puede iniciar una nueva iteración de cada tercer ciclo con la canalización.
Suponiendo que algo menos que el almacenamiento en caché completo en a
hace que la aritmética y las instrucciones cuenten incluso menos de un cuello de botella. No me sorprendería que la mayor parte del aumento de velocidad en mi código proviene de la emisión de un menor número de cargas a b
c
y espacio para que más está libre para realizar un seguimiento y especular últimos fallos de caché en a
.
de hardware más amplia podría tener más diferencia. Un sistema Nehalem con tres canales de DDR3-1333 necesitaría mover 3 * 10667/2.66 = 12.6 bytes por ciclo para saturar el ancho de banda de la memoria. Eso sería imposible para un único hilo si a
encaja en el caché, pero a 64 bytes carece de un caché de línea en el vector se agrega rápidamente, solo una de las cuatro cargas en mi bucle falta en cachés trae el ancho de banda promedio requerido a 16 bytes /ciclo.
¿Cuál es la distribución de los índices en b? – MSN
Desconocido, hasta el tiempo de ejecución. – Mike
Simplemente curioso, ¿las siguientes sugerencias ayudaron a acelerar tu código? – celion