Tengo la representación de columna dispersa comprimida (csc) de la matriz triangular inferior nxn A con ceros en la diagonal principal, y me gustaría resolver b en
(A + I)' * x = b
Esta es la rutina que tengo para calcular esto:
void backsolve(const int*__restrict__ Lp,
const int*__restrict__ Li,
const double*__restrict__ Lx,
const int n,
double*__restrict__ x) {
for (int i=n-1; i>=0; --i) {
for (int j=Lp[i]; j<Lp[i+1]; ++j) {
x[i] -= Lx[j] * x[Li[j]];
}
}
}
Por lo tanto, b
se pasa a través del argumento x
y se sobrescribe con la solución. Lp
, Li
, Lx
Son, respectivamente, las hileras, índices, y los punteros de datos en la representación csc estándar de matrices dispersas. Esta función es el punto de acceso superior en el programa, con la línea
x[i] -= Lx[j] * x[Li[j]];
siendo la mayor parte del tiempo dedicado. Compilando con gcc-8.3 -O3 -mfma -mavx -mavx512f
da
backsolve(int const*, int const*, double const*, int, double*):
lea eax, [rcx-1]
movsx r11, eax
lea r9, [r8+r11*8]
test eax, eax
js .L9
.L5:
movsx rax, DWORD PTR [rdi+r11*4]
mov r10d, DWORD PTR [rdi+4+r11*4]
cmp eax, r10d
jge .L6
vmovsd xmm0, QWORD PTR [r9]
.L7:
movsx rcx, DWORD PTR [rsi+rax*4]
vmovsd xmm1, QWORD PTR [rdx+rax*8]
add rax, 1
vfnmadd231sd xmm0, xmm1, QWORD PTR [r8+rcx*8]
vmovsd QWORD PTR [r9], xmm0
cmp r10d, eax
jg .L7
.L6:
sub r11, 1
sub r9, 8
test r11d, r11d
jns .L5
ret
.L9:
ret
Según vtune,
vmovsd QWORD PTR [r9], xmm0
Es la parte más lenta. Casi no tengo experiencia con el ensamblaje, y no sé cómo diagnosticar u optimizar aún más esta operación. He intentado compilar con diferentes indicadores para habilitar / deshabilitar SSE, FMA, etc., pero nada ha funcionado.
Procesador: Xeon Skylake
Pregunta ¿Qué puedo hacer para optimizar esta función?
fuente
i >= Li[j]
para todosj
en el circuito interno?Li[j]
sean disjuntas dei
, verificar el supuesto con instrucciones de detección de conflictos, verificar que todas lasi
s sean disjuntas, calcular, almacenar en dispersión los resultados. Si se detecta algún conflicto, recurra a la implementación escalar.i < Li[j] < n
. Se actualizó la pregunta para mencionar la naturaleza triangular inferior de A.Respuestas:
Esto debería depender bastante del patrón de dispersión exacto de la matriz y la plataforma que se utiliza. Probé algunas cosas con
gcc 8.3.0
los indicadores del compilador-O3 -march=native
(que está-march=skylake
en mi CPU) en el triángulo inferior de esta matriz de dimensión 3006 con 19554 entradas distintas de cero. Espero que esto esté algo cerca de su configuración, pero en cualquier caso espero que esto pueda darle una idea de dónde comenzar.Para el tiempo utilicé google / benchmark con este archivo fuente . Define
benchBacksolveBaseline
qué puntos de referencia la implementación dada en la pregunta ybenchBacksolveOptimized
qué puntos de referencia las implementaciones "optimizadas" propuestas. También haybenchFillRhs
que compara por separado la función que se utiliza en ambos para generar algunos valores no completamente triviales para el lado derecho. Para obtener el tiempo de las soluciones "puras",benchFillRhs
se debe restar el tiempo que lleva.1. Iterando estrictamente al revés
El bucle externo en su implementación itera a través de las columnas hacia atrás, mientras que el bucle interno itera a través de la columna actual hacia adelante. Parece que sería más consistente iterar a través de cada columna también hacia atrás:
Esto apenas cambia el ensamblaje ( https://godbolt.org/z/CBZAT5 ), pero los tiempos de referencia muestran una mejora considerable:
Supongo que esto es causado por un acceso a la caché más predecible, pero no lo examiné mucho más.
2. Menos cargas / tiendas en bucle interno
Como A es triangular inferior, tenemos
i < Li[j]
. Por lo tanto, sabemos quex[Li[j]]
no cambiará debido a los cambiosx[i]
en el bucle interno. Podemos poner este conocimiento en nuestra implementación mediante el uso de una variable temporal:Esto hace que
gcc 8.3.0
mover la tienda a la memoria desde el interior del bucle interno directamente después de su finalización ( https://godbolt.org/z/vM4gPD ). El punto de referencia para la matriz de prueba en mi sistema muestra una pequeña mejora:3. Desenrollar el bucle
Si bien
clang
ya comienza a desenrollar el bucle después del primer cambio de código sugerido,gcc 8.3.0
aún no lo ha hecho. Así que vamos a intentarlo pasando adicionalmente-funroll-loops
.Tenga en cuenta que la línea de base también mejora, ya que el ciclo en esa implementación también se desenrolla. Nuestra versión optimizada también se beneficia un poco del desenrollado del bucle, pero quizás no tanto como nos hubiera gustado. Mirando el ensamblaje generado ( https://godbolt.org/z/_LJC5f ), parece que
gcc
podría haber ido un poco lejos con 8 desenrollamientos. Para mi configuración, de hecho, puedo hacerlo un poco mejor con solo un desenrollado manual simple. Así que suelte la bandera-funroll-loops
nuevamente e implemente el desenrollado con algo como esto:Con eso mido:
Otros algoritmos
Todas estas versiones todavía usan la misma implementación simple de la resolución hacia atrás en la estructura de matriz dispersa. Inherentemente, operar en estructuras de matriz dispersas como estas puede tener problemas significativos con el tráfico de memoria. Al menos para las factorizaciones matriciales, existen métodos más sofisticados que operan en submatrices densas que se ensamblan a partir de la estructura dispersa. Los ejemplos son métodos supernodales y multifrontales. Estoy un poco confuso en esto, pero creo que tales métodos también aplicarán esta idea al diseño y usarán operaciones de matriz densas para soluciones triangulares inferiores hacia atrás (por ejemplo, para factorizaciones de tipo Cholesky). Por lo tanto, podría valer la pena analizar ese tipo de métodos, si no se ve obligado a seguir el método simple que funciona directamente en la estructura dispersa. Ver por ejemplo esta encuestapor Davis.
fuente
-ffast-math
) clang utilizará múltiples acumuladores. GCC se desenrollará pero no se molestará en crear múltiples cadenas de dependencia para ocultar la latencia de ALU, lo que anulará la mayor parte del propósito de los bucles de reducción comoxi_temp -=
. Aunque si la mejora 2. compilara de la manera que esperaríamos, sacando de la ruta crítica la latencia de almacenamiento / reenvío de la tienda, pero acelerada por mucho menos que un factor de 2, parece que la latencia de FP no es un gran cuello de botella (en cambio memoria / errores de caché), o que las cadenas dep son lo suficientemente cortas para que el ejecutivo de OoO se oculte.Puede reducir algunos ciclos utilizando en
unsigned
lugar deint
los tipos de índice, que deben ser de>= 0
todos modos:Compilando con compilador explorador de Godbolt de código se muestra ligeramente diferente para el Innerloop, lo que podría hacer un mejor uso de la tubería de la CPU. No puedo probar, pero puedes intentarlo.
Aquí está el código generado para el bucle interno:
fuente
unsigned
lugar desize_t
evitar la extensión del letrero sin afectar la memoria caché, y el código es ligeramente diferente, lo que potencialmente permite un mejor uso de la canalización.unsigned
, pero no veo nada que parezca una mejor canalización con él. Para mí, se ve un poco peor que el códigoint
osize_t
. De todos modos, también parece quegcc
está tratando de desperdiciar memoria al usarincq %rax
withgcc-9.2.1 -march=skylake-avx512
para los casosint
yunsigned
, dondeincl %rax
ahorraría un byte rex. Hoy estoy un poco impresionado con gcc.