style - ¿Por qué mulss solo toma 3 ciclos en Haswell, diferente de las tablas de instrucciones de Agner?
tags en html (1)
Mire su bucle nuevamente:
movss xmm1, src
no depende del valor anterior de
xmm1
, porque su destino es de solo escritura
.
El
mulss
cada iteración es independiente.
La ejecución fuera de orden puede explotar ese paralelismo en el nivel de instrucción y lo hace, por lo que definitivamente no tiene cuellos de botella en la latencia de
mulss
.
Lectura opcional: en términos de arquitectura de computadora: el cambio de nombre del registro evita el riesgo de datos anti-dependencia de WAR de reutilizar el mismo registro arquitectónico. (Algunos esquemas de canalización + seguimiento de dependencias antes del cambio de nombre del registro no resolvieron todos los problemas, por lo que el campo de la arquitectura de la computadora hace gran parte de los diferentes tipos de riesgos de datos.
El cambio de nombre de registro con
el algoritmo de Tomasulo
hace que todo desaparezca, excepto las dependencias verdaderas reales (lectura después de escritura), por lo que cualquier instrucción en la que el destino no sea también un registro fuente no tiene interacción con la cadena de dependencia que involucra el valor anterior de ese registro.
(Excepto por las dependencias falsas, como
popcnt
en las CPU Intel
, y escribir solo una parte de un registro sin borrar el resto (como
mov al, 5
o
sqrtss xmm2, xmm1
). Relacionado:
¿Por qué la mayoría de las instrucciones x64
sqrtss xmm2, xmm1
cero la parte superior de un 32? bit de registro
).
Regresar a su código:
.L13:
movss xmm1, DWORD PTR [rdi+rax*4]
mulss xmm1, DWORD PTR [rsi+rax*4]
add rax, 1
cmp cx, ax
addss xmm0, xmm1
jg .L13
Las dependencias transportadas en bucle (de una iteración a la siguiente) son cada una:
-
xmm0
, leído y escrito poraddss xmm0, xmm1
, que tiene latencia de 3 ciclos en Haswell. -
rax
, leído y escrito poradd rax, 1
. Latencia 1c, por lo que no es la ruta crítica.
Parece que midió el tiempo de ejecución / recuento de ciclos correctamente, porque los cuellos de botella del bucle en el 3c
addss
latencia.
Esto se espera: la dependencia en serie en un producto de puntos es la suma en una suma única (también conocida como la reducción), no las multiplicaciones entre elementos vectoriales.
Ese es, con mucho, el cuello de botella dominante para este ciclo, a pesar de varias ineficiencias menores:
short i
el tonto
cmp cx, ax
, que toma un prefijo de tamaño de operando adicional.
Afortunadamente, gcc logró evitar
add ax, 1
, porque el desbordamiento firmado es Comportamiento indefinido en C.
Por lo tanto, el optimizador puede suponer que no sucede
.
(actualización:
las reglas de promoción de enteros lo hacen diferente para
short
, por lo que UB no entra en él, pero gcc aún puede optimizarse legalmente. Cosas bastante extravagantes).
Si hubiera compilado con
-mtune=intel
, o mejor,
-march=haswell
, gcc habría puesto el
cmp
y el
jg
uno al lado del otro donde podrían fusionarse con macro.
No estoy seguro de por qué tiene un
*
en su tabla en el
cmp
y
add
instrucciones.
(Actualización: estaba simplemente adivinando que estaba usando una notación como
IACA
, pero aparentemente no lo estaba haciendo).
Ninguno de ellos se fusiona.
La única fusión que ocurre es la micro fusión de
mulss xmm1, [rsi+rax*4]
.
Y dado que es una instrucción ALU de 2 operandos con un registro de destino de lectura-modificación-escritura, permanece macro-fusionada incluso en el ROB en Haswell.
(Sandybridge lo deslaminaría en el momento de la emisión).
Tenga en cuenta que
vmulss xmm1, xmm1, [rsi+rax*4]
se
vmulss xmm1, xmm1, [rsi+rax*4]
en Haswell
.
Nada de esto es realmente importante, ya que simplemente cuellos de botella en la latencia de agregar FP, mucho más lento que cualquier límite de rendimiento de uop.
Sin
-ffast-math
, no hay nada que los compiladores puedan hacer.
Con
-ffast-math
, el
-ffast-math
normalmente se desenrollará con múltiples acumuladores, y se vectorizará automáticamente para que sean acumuladores vectoriales.
Por lo tanto, probablemente pueda saturar el límite de rendimiento de Haswell de 1 vector o agregar escalar FP por reloj, si acierta en el caché L1D.
Con FMA con una latencia de 5c y un rendimiento de 0.5c en Haswell, necesitaría 10 acumuladores para mantener 10 FMA en vuelo y maximizar el rendimiento de FMA manteniendo p0 / p1 saturado con FMA. (Skylake redujo la latencia de FMA a 4 ciclos, y ejecuta multiplicar, agregar y FMA en las unidades de FMA. Por lo tanto, en realidad tiene una latencia de agregar más alta que Haswell).
(Usted tiene un cuello de botella en las cargas, porque necesita dos cargas para cada FMA. En otros casos, en realidad puede obtener un mayor rendimiento al reemplazar algunas instrucciones
vaddps
con una FMA con un multiplicador de 1.0. Esto significa más latencia para ocultar, por lo que es mejor en un algoritmo más complejo donde tienes un complemento que no está en la ruta crítica en primer lugar).
Re: uops por puerto :
hay 1.19 uops por bucle en el puerto 5, es mucho más de lo esperado 0.5, es cuestión de que el despachador de uops intente hacer uops en cada puerto
Si, algo así.
Los uops no se asignan al azar, o de alguna manera se distribuyen de manera uniforme en todos los puertos en los que se
pueden
ejecutar.
Asumiste que
add
y
cmp
uops se distribuirían de manera uniforme en p0156, pero ese no es el caso.
La etapa de problema asigna uops a los puertos en función de cuántos uops ya están esperando ese puerto.
Dado que las
addss
solo pueden ejecutarse en p1 (y es el cuello de botella del bucle), generalmente se emiten muchos u1 pops pero no se ejecutan.
Tan pocos otros uops serán programados para port1.
(Esto incluye
mulss
: la mayoría de los
mulss
uops terminarán programados en el puerto 0.)
Las ramas tomadas solo pueden ejecutarse en el puerto 6. El puerto 5 no tiene ningún uops en este bucle que solo puede ejecutarse allí, por lo que termina atrayendo a muchos de los uops de muchos puertos.
El planificador (que selecciona los uops de dominio no fusionado de la estación de reserva) no es lo suficientemente inteligente como para ejecutar la ruta crítica primero, por lo que este algoritmo de asignación reduce la latencia de conflicto de recursos (otros uops roban el puerto1 en ciclos cuando un
addss
podría tener correr).
También es útil en casos en los que se cuela el rendimiento de un puerto determinado.
La programación de uops ya asignados normalmente es la más antigua lista, según tengo entendido. Este algoritmo simple no es sorprendente, ya que tiene que elegir un uop con sus entradas listas para cada puerto de un RS de 60 entradas en cada ciclo de reloj, sin derretir su CPU. La maquinaria fuera de servicio que encuentra y explota el ILP es uno de los costos de energía significativos en una CPU moderna, comparable a las unidades de ejecución que hacen el trabajo real.
Relacionado / más detalles: ¿Cómo se programan exactamente los uops x86?
Más material de análisis de rendimiento:
Además de errores de caché / errores de bifurcación, los tres principales cuellos de botella posibles para los bucles vinculados a la CPU son:
- cadenas de dependencia (como en este caso)
- rendimiento de front-end (máximo de 4 uops de dominio fusionado emitidos por reloj en Haswell)
- cuellos de botella en el puerto de ejecución, como si muchos uops necesitan p0 / p1 o p2 / p3, como en su bucle desenrollado. Cuente uops de dominio no fusionado para puertos específicos. En general, puede asumir la distribución del mejor de los casos, con uops que pueden ejecutarse en otros puertos sin robar los puertos ocupados muy a menudo, pero sucede algo.
Un cuerpo de bucle o un bloque corto de código puede caracterizarse aproximadamente por 3 cosas: recuento de UOP de dominio fusionado, recuento de dominio no fusionado de las unidades de ejecución en las que puede ejecutarse y latencia total de ruta crítica suponiendo la mejor programación para su ruta crítica . (O latencias de cada entrada A / B / C a la salida ...)
Por ejemplo, para hacer las tres cosas para comparar algunas secuencias cortas, vea mi respuesta en ¿Cuál es la forma eficiente de contar los bits establecidos en una posición o inferior?
Para bucles cortos, las CPU modernas tienen suficientes recursos de ejecución fuera de orden (tamaño de archivo de registro físico para que el cambio de nombre no se quede sin registros, tamaño ROB) para tener suficientes iteraciones de un bucle en vuelo para encontrar todo el paralelismo. Pero a medida que las cadenas de dependencia dentro de los bucles se alargan, eventualmente se agotan. Consulte Medición de la capacidad del búfer de reordenamiento para obtener algunos detalles sobre lo que sucede cuando una CPU se queda sin registros para cambiar el nombre.
Consulte también muchos enlaces de rendimiento y referencia en el wiki de etiquetas x86 .
Ajuste su bucle FMA:
Sí, el producto punto en Haswell tendrá un cuello de botella en el rendimiento L1D a solo la mitad del rendimiento de las unidades FMA, ya que requiere dos cargas por multiplicación + adición.
Si estabas haciendo
B[i] = x * A[i] + y;
o
sum(A[i]^2)
, podría saturar el rendimiento de FMA.
Parece que todavía está tratando de evitar la reutilización de registros incluso en casos de solo escritura como el destino de una carga
vmovaps
, por lo que se quedó sin registros después de desenrollar por 8
.
Eso está bien, pero podría importar para otros casos.
Además, el uso de
ymm8-15
puede aumentar ligeramente el tamaño del código si significa que se necesita un prefijo VEX de 3 bytes en lugar de 2 bytes.
Dato
vpxor ymm7,ymm7,ymm8
:
vpxor ymm7,ymm7,ymm8
necesita un VEX de 3 bytes, mientras que
vpxor ymm8,ymm8,ymm7
solo necesita un prefijo VEX de 2 bytes.
Para operaciones conmutativas, ordene los registros de origen de mayor a menor.
Nuestro cuello de botella de carga significa que el rendimiento de FMA en el mejor de los casos es la mitad del máximo, por lo que necesitamos al menos 5 acumuladores de vectores para ocultar su latencia. 8 es bueno, por lo que hay una gran holgura en las cadenas de dependencia para permitirles ponerse al día después de cualquier retraso de latencia inesperada o competencia por p0 / p1. 7 o incluso 6 también estaría bien: su factor de desenrollado no tiene que ser una potencia de 2.
Desenrollar exactamente 5 significaría que también está en el cuello de botella para las cadenas de dependencia . Cada vez que un FMA no se ejecuta en el ciclo exacto, su entrada está lista significa un ciclo perdido en esa cadena de dependencia. Esto puede suceder si una carga es lenta (por ejemplo, se pierde en el caché L1 y tiene que esperar a L2), o si las cargas se completan fuera de servicio y un FMA de otra cadena de dependencia roba el puerto para el que se programó este FMA. (Recuerde que la programación ocurre en el momento de la emisión, por lo que los uops que se encuentran en el programador son port0 FMA o port1 FMA, no un FMA que puede tomar el puerto que esté inactivo).
Si deja un poco de holgura en las cadenas de dependencia, la ejecución fuera de orden puede "ponerse al día" con las FMA, ya que no tendrán cuellos de botella en el rendimiento o la latencia, solo esperarán los resultados de la carga. @Forward descubrió (en una actualización de la pregunta) que el desenrollamiento en 5 redujo el rendimiento del 93% del rendimiento de L1D al 89.5% para este bucle.
Mi conjetura es que desenrollar en 6 (uno más que el mínimo para ocultar la latencia) estaría bien aquí, y obtendría el mismo rendimiento que desenrollar en 8. Si estuviéramos más cerca de maximizar el rendimiento de FMA (en lugar de limitar la carga) rendimiento), uno más que el mínimo podría no ser suficiente.
actualización: la prueba experimental de @ Forward muestra que mi suposición fue incorrecta
.
No hay una gran diferencia entre unroll5 y unroll6.
Además, unroll15 es dos veces más cercano que unroll8 al rendimiento máximo teórico de 2x 256b cargas por reloj.
La medición con solo cargas independientes en el bucle, o con cargas independientes y FMA de solo registro, nos dirá cuánto de eso se debe a la interacción con la cadena de dependencia de FMA.
Incluso el mejor de los casos no obtendrá un rendimiento perfecto del 100%, aunque solo sea por errores de medición e interrupciones debido a interrupciones del temporizador.
(El
perf
Linux solo mide los ciclos de espacio de usuario a menos que lo ejecute como root, pero el tiempo aún incluye el tiempo dedicado a los manejadores de interrupciones. Es por eso que la frecuencia de su CPU puede informarse como 3.87 GHz cuando se ejecuta como no root, pero 3.900 GHz cuando se ejecuta como raíz y
cycles
medición en lugar de
cycles:u
.)
No tenemos cuellos de botella en el rendimiento del front-end, pero podemos reducir el conteo de UOP de dominio fusionado evitando los modos de direccionamiento indexado para instrucciones que no son
mov
.
Menos es mejor y hace que esto sea más
amigable con el hyperthreading
cuando se comparte un núcleo con algo más que esto.
La manera simple es simplemente hacer dos incrementos de puntero dentro del bucle. La forma complicada es un buen truco para indexar una matriz en relación con la otra:
;; input pointers for x[] and y[] in rdi and rsi
;; size_t n in rdx
;;; zero ymm1..8, or load+vmulps into them
add rdx, rsi ; end_y
; lea rdx, [rdx+rsi-252] to break out of the unrolled loop before going off the end, with odd n
sub rdi, rsi ; index x[] relative to y[], saving one pointer increment
.unroll8:
vmovaps ymm0, [rdi+rsi] ; *px, actually py[xy_offset]
vfmadd231ps ymm1, ymm0, [rsi] ; *py
vmovaps ymm0, [rdi+rsi+32] ; write-only reuse of ymm0
vfmadd231ps ymm2, ymm0, [rsi+32]
vmovaps ymm0, [rdi+rsi+64]
vfmadd231ps ymm3, ymm0, [rsi+64]
vmovaps ymm0, [rdi+rsi+96]
vfmadd231ps ymm4, ymm0, [rsi+96]
add rsi, 256 ; pointer-increment here
; so the following instructions can still use disp8 in their addressing modes: [-128 .. +127] instead of disp32
; smaller code-size helps in the big picture, but not for a micro-benchmark
vmovaps ymm0, [rdi+rsi+128-256] ; be pedantic in the source about compensating for the pointer-increment
vfmadd231ps ymm5, ymm0, [rsi+128-256]
vmovaps ymm0, [rdi+rsi+160-256]
vfmadd231ps ymm6, ymm0, [rsi+160-256]
vmovaps ymm0, [rdi+rsi-64] ; or not
vfmadd231ps ymm7, ymm0, [rsi-64]
vmovaps ymm0, [rdi+rsi-32]
vfmadd231ps ymm8, ymm0, [rsi-32]
cmp rsi, rdx
jb .unroll8 ; } while(py < endy);
El uso de un modo de direccionamiento no indexado como el operando de memoria para
vfmaddps
permite permanecer micro-fusionado en el núcleo fuera de orden, en lugar de no ser laminado en cuestión.
Micro fusión y modos de direccionamiento
Entonces mi bucle es 18 uops de dominio fusionado para 8 vectores. El tuyo toma 3 uops de dominio fusionado para cada par vmovaps + vfmaddps, en lugar de 2, debido a la no laminación de los modos de direccionamiento indexado. Ambos, por supuesto, todavía tienen 2 uops de carga de dominio no fusionado (puerto2 / 3) por par, por lo que sigue siendo el cuello de botella.
Menos Uops de dominio fusionado permite que la ejecución fuera de orden vea más iteraciones por delante, lo que podría ayudarlo a absorber mejor los errores de caché. Sin embargo, es algo menor cuando tenemos un cuello de botella en una unidad de ejecución (cargar uops en este caso) incluso sin errores de caché. Pero con hyperthreading, solo obtienes cada dos ciclos de ancho de banda de problemas de front-end a menos que el otro hilo esté estancado. Si no está compitiendo demasiado por la carga y p0 / 1, menos uops de dominio fusionado permitirán que este bucle se ejecute más rápido mientras comparte un núcleo. (por ejemplo, ¿quizás el otro hiperproceso está ejecutando mucho puerto 5 / puerto 6 y almacena uops?)
Dado que la deslaminación ocurre después del uop-cache, su versión no ocupa espacio adicional en el uop cache. Un disp32 con cada uop está bien, y no ocupa espacio extra. Pero el tamaño de código más voluminoso significa que es menos probable que el caché uop se empaque tan eficientemente, ya que alcanzará los límites de 32B antes de que las líneas de caché uop estén llenas con más frecuencia. (En realidad, un código más pequeño tampoco garantiza una mejor. Las instrucciones más pequeñas podrían llevar a llenar una línea de caché uop y necesitar una entrada en otra línea antes de cruzar un límite de 32B). Este pequeño bucle puede ejecutarse desde el búfer de bucle invertido (LSD), por lo que Afortunadamente, el uop-cache no es un factor.
Luego, después del ciclo: la limpieza eficiente es la parte difícil de la vectorización eficiente para arreglos pequeños que podrían no ser múltiplos del factor de desenrollado o especialmente el ancho del vector
...
jb
;; If `n` might not be a multiple of 4x 8 floats, put cleanup code here
;; to do the last few ymm or xmm vectors, then scalar or an unaligned last vector + mask.
; reduce down to a single vector, with a tree of dependencies
vaddps ymm1, ymm2, ymm1
vaddps ymm3, ymm4, ymm3
vaddps ymm5, ymm6, ymm5
vaddps ymm7, ymm8, ymm7
vaddps ymm0, ymm3, ymm1
vaddps ymm1, ymm7, ymm5
vaddps ymm0, ymm1, ymm0
; horizontal within that vector, low_half += high_half until we''re down to 1
vextractf128 xmm1, ymm0, 1
vaddps xmm0, xmm0, xmm1
vmovhlps xmm1, xmm0, xmm0
vaddps xmm0, xmm0, xmm1
vmovshdup xmm1, xmm0
vaddss xmm0, xmm1
; this is faster than 2x vhaddps
vzeroupper ; important if returning to non-AVX-aware code after using ymm regs.
ret ; with the scalar result in xmm0
Para obtener más información sobre la suma horizontal al final, consulte la
forma más rápida de hacer una suma de vector flotante horizontal en x86
.
Los dos 128b shuffles que utilicé ni siquiera necesitan un byte de control inmediato, por lo que ahorra 2 bytes de tamaño de código en comparación con los
shufps
más obvios.
(Y 4 bytes de tamaño de código frente a
vpermilps
, porque ese código de operación siempre necesita un prefijo VEX de 3 bytes, así como un inmediato).
El material AVX de 3 operandos es
muy
bueno en comparación con el SSE, especialmente cuando se escribe en C con intrínsecos, por lo que no puede elegir fácilmente un registro frío para
movhlps
.
Soy un novato en la optimización de instrucciones.
Hice un análisis simple en una función simple dotp que se usa para obtener el producto punto de dos matrices flotantes.
El código C es el siguiente:
float dotp(
const float x[],
const float y[],
const short n
)
{
short i;
float suma;
suma = 0.0f;
for(i=0; i<n; i++)
{
suma += x[i] * y[i];
}
return suma;
}
Utilizo el marco de prueba proporcionado por Agner Fog en la web testp .
Las matrices que se utilizan en este caso están alineadas:
int n = 2048;
float* z2 = (float*)_mm_malloc(sizeof(float)*n, 64);
char *mem = (char*)_mm_malloc(1<<18,4096);
char *a = mem;
char *b = a+n*sizeof(float);
char *c = b+n*sizeof(float);
float *x = (float*)a;
float *y = (float*)b;
float *z = (float*)c;
Luego llamo a la función dotp, n = 2048, repita = 100000:
for (i = 0; i < repeat; i++)
{
sum = dotp(x,y,n);
}
Lo compilo con gcc 4.8.3, con la opción de compilación -O3.
Compilo esta aplicación en una computadora que no admite instrucciones FMA, por lo que puede ver que solo hay instrucciones SSE.
El código de ensamblaje:
.L13:
movss xmm1, DWORD PTR [rdi+rax*4]
mulss xmm1, DWORD PTR [rsi+rax*4]
add rax, 1
cmp cx, ax
addss xmm0, xmm1
jg .L13
Hago un análisis:
μops-fused la 0 1 2 3 4 5 6 7
movss 1 3 0.5 0.5
mulss 1 5 0.5 0.5 0.5 0.5
add 1 1 0.25 0.25 0.25 0.25
cmp 1 1 0.25 0.25 0.25 0.25
addss 1 3 1
jg 1 1 1 -----------------------------------------------------------------------------
total 6 5 1 2 1 1 0.5 1.5
Después de correr, obtenemos el resultado:
Clock | Core cyc | Instruct | BrTaken | uop p0 | uop p1
--------------------------------------------------------------------
542177906 |609942404 |1230100389 |205000027 |261069369 |205511063
--------------------------------------------------------------------
2.64 | 2.97 | 6.00 | 1 | 1.27 | 1.00
uop p2 | uop p3 | uop p4 | uop p5 | uop p6 | uop p7
-----------------------------------------------------------------------
205185258 | 205188997 | 100833 | 245370353 | 313581694 | 844
-----------------------------------------------------------------------
1.00 | 1.00 | 0.00 | 1.19 | 1.52 | 0.00
La segunda línea es el valor leído de los registros de Intel; la tercera línea se divide por el número de sucursal, "BrTaken".
Así podemos ver, en el bucle hay 6 instrucciones, 7 uops, de acuerdo con el análisis.
Los números de uops ejecutados en port0 port1 port 5 port6 son similares a lo que dice el análisis. Creo que tal vez el planificador de Uops hace esto, puede intentar equilibrar las cargas en los puertos, ¿estoy en lo cierto?
No entiendo absolutamente por qué solo hay unos 3 ciclos por ciclo.
De acuerdo con la
tabla de instrucciones
de Agner, la latencia de la instrucción
mulss
es 5, y existen dependencias entre los bucles, por lo que, por lo que veo, debería tomar al menos 5 ciclos por bucle.
¿Alguien podría arrojar alguna idea?
================================================== ================
Intenté escribir una versión optimizada de esta función en nasm, desenrollando el bucle por un factor de 8 y usando la instrucción
vfmadd231ps
:
.L2:
vmovaps ymm1, [rdi+rax]
vfmadd231ps ymm0, ymm1, [rsi+rax]
vmovaps ymm2, [rdi+rax+32]
vfmadd231ps ymm3, ymm2, [rsi+rax+32]
vmovaps ymm4, [rdi+rax+64]
vfmadd231ps ymm5, ymm4, [rsi+rax+64]
vmovaps ymm6, [rdi+rax+96]
vfmadd231ps ymm7, ymm6, [rsi+rax+96]
vmovaps ymm8, [rdi+rax+128]
vfmadd231ps ymm9, ymm8, [rsi+rax+128]
vmovaps ymm10, [rdi+rax+160]
vfmadd231ps ymm11, ymm10, [rsi+rax+160]
vmovaps ymm12, [rdi+rax+192]
vfmadd231ps ymm13, ymm12, [rsi+rax+192]
vmovaps ymm14, [rdi+rax+224]
vfmadd231ps ymm15, ymm14, [rsi+rax+224]
add rax, 256
jne .L2
El resultado:
Clock | Core cyc | Instruct | BrTaken | uop p0 | uop p1
------------------------------------------------------------------------
24371315 | 27477805| 59400061 | 3200001 | 14679543 | 11011601
------------------------------------------------------------------------
7.62 | 8.59 | 18.56 | 1 | 4.59 | 3.44
uop p2 | uop p3 | uop p4 | uop p5 | uop p6 | uop p7
-------------------------------------------------------------------------
25960380 |26000252 | 47 | 537 | 3301043 | 10
------------------------------------------------------------------------------
8.11 |8.13 | 0.00 | 0.00 | 1.03 | 0.00
Entonces podemos ver que el caché de datos L1 alcanza 2 * 256bit / 8.59, está muy cerca del pico 2 * 256/8, el uso es de aproximadamente el 93%, la unidad FMA solo usó 8 / 8.59, el pico es 2 * 8 / 8, el uso es del 47%.
Así que creo que he llegado al cuello de botella L1D como Peter Cordes espera.
================================================== ================
Un agradecimiento especial a Boann, corrige tantos errores gramaticales en mi pregunta.
================================================== ===============
De la respuesta de Peter, entiendo que solo el registro "leído y escrito" sería la dependencia, los registros "solo de escritor" no serían la dependencia.
Así que trato de reducir los registros utilizados en el bucle, y trato de desenrollar en 5, si todo está bien, debería encontrar el mismo cuello de botella, L1D.
.L2:
vmovaps ymm0, [rdi+rax]
vfmadd231ps ymm1, ymm0, [rsi+rax]
vmovaps ymm0, [rdi+rax+32]
vfmadd231ps ymm2, ymm0, [rsi+rax+32]
vmovaps ymm0, [rdi+rax+64]
vfmadd231ps ymm3, ymm0, [rsi+rax+64]
vmovaps ymm0, [rdi+rax+96]
vfmadd231ps ymm4, ymm0, [rsi+rax+96]
vmovaps ymm0, [rdi+rax+128]
vfmadd231ps ymm5, ymm0, [rsi+rax+128]
add rax, 160 ;n = n+32
jne .L2
El resultado:
Clock | Core cyc | Instruct | BrTaken | uop p0 | uop p1
------------------------------------------------------------------------
25332590 | 28547345 | 63700051 | 5100001 | 14951738 | 10549694
------------------------------------------------------------------------
4.97 | 5.60 | 12.49 | 1 | 2.93 | 2.07
uop p2 |uop p3 | uop p4 | uop p5 |uop p6 | uop p7
------------------------------------------------------------------------------
25900132 |25900132 | 50 | 683 | 5400909 | 9
-------------------------------------------------------------------------------
5.08 |5.08 | 0.00 | 0.00 |1.06 | 0.00
Podemos ver 5 / 5.60 = 89.45%, es un poco más pequeño que urlar por 8, ¿hay algo mal?
================================================== ===============
Intento desenrollar el bucle por 6, 7 y 15, para ver el resultado. También me desenrollo por 5 y 8 nuevamente, para confirmar el resultado dos veces.
El resultado es el siguiente, podemos ver esta vez que el resultado es mucho mejor que antes.
Aunque el resultado no es estable, el factor de desenrollado es mayor y el resultado es mejor.
| L1D bandwidth | CodeMiss | L1D Miss | L2 Miss
----------------------------------------------------------------------------
unroll5 | 91.86% ~ 91.94% | 3~33 | 272~888 | 17~223
--------------------------------------------------------------------------
unroll6 | 92.93% ~ 93.00% | 4~30 | 481~1432 | 26~213
--------------------------------------------------------------------------
unroll7 | 92.29% ~ 92.65% | 5~28 | 336~1736 | 14~257
--------------------------------------------------------------------------
unroll8 | 95.10% ~ 97.68% | 4~23 | 363~780 | 42~132
--------------------------------------------------------------------------
unroll15 | 97.95% ~ 98.16% | 5~28 | 651~1295 | 29~68
================================================== ===================
Intento compilar la función con gcc 7.1 en la web " https://gcc.godbolt.org "
La opción de compilación es "-O3 -march = haswell -mtune = intel", que es similar a gcc 4.8.3.
.L3:
vmovss xmm1, DWORD PTR [rdi+rax]
vfmadd231ss xmm0, xmm1, DWORD PTR [rsi+rax]
add rax, 4
cmp rdx, rax
jne .L3
ret