Estoy muy interesado en optimizar al máximo la resolución de sistemas lineales para matrices pequeñas (10x10), a veces llamadas matrices pequeñas . ¿Hay una solución lista para esto? La matriz se puede suponer no singular.
Este solucionador se ejecutará más de 1 000 000 de veces en microsegundos en una CPU Intel. Estoy hablando del nivel de optimización utilizado en los juegos de computadora. No importa si lo codifico en ensamblaje y arquitectura específica, o estudio reducciones de precisión o confiabilidad y uso hacks de punto flotante (uso el indicador de compilación -ffast-math, no hay problema). ¡La solución incluso puede fallar durante aproximadamente el 20% del tiempo!
ParcialPivLu de Eigen es el más rápido en mi punto de referencia actual, superando a LAPACK cuando está optimizado con -O3 y un buen compilador. Pero ahora estoy a punto de crear un solucionador lineal personalizado. Cualquier consejo sería muy apreciado. Haré que mi solución sea de código abierto y reconoceré ideas clave en publicaciones, etc.
Relacionado: Velocidad de resolución del sistema lineal con matriz diagonal de bloques ¿Cuál es el método más rápido para invertir millones de matrices? https://stackoverflow.com/q/50909385/1489510
Respuestas:
El uso de un tipo de matriz Eigen donde el número de filas y columnas se codifica en el tipo en tiempo de compilación le da una ventaja sobre LAPACK, donde el tamaño de la matriz se conoce solo en tiempo de ejecución. Esta información adicional permite que el compilador realice el desenrollado de bucle completo o parcial, eliminando muchas instrucciones de ramificación. Si está buscando usar una biblioteca existente en lugar de escribir sus propios núcleos, probablemente sea esencial tener un tipo de datos donde el tamaño de la matriz se pueda incluir como parámetros de plantilla de C ++. La única otra biblioteca que conozco que hace esto es Blaze , por lo que podría valer la pena comparar con Eigen.
Si decide implementar su propia implementación, es posible que lo que hace PETSc para su formato CSR de bloque sea un ejemplo útil, aunque PETSc en sí mismo probablemente no sea la herramienta adecuada para lo que tiene en mente. En lugar de escribir un bucle, escriben explícitamente cada operación para pequeñas multiplicaciones de matriz-vector (vea este archivo en su repositorio). Esto garantiza que no hay instrucciones de bifurcación como las que podría obtener con un bucle. Las versiones del código con instrucciones AVX son un buen ejemplo de cómo usar realmente extensiones vectoriales. Por ejemplo, esta función usa el
__m256d
tipo de datos para operar simultáneamente en cuatro dobles al mismo tiempo. Puede obtener un aumento de rendimiento apreciable al escribir explícitamente todas las operaciones utilizando extensiones de vector, solo para la factorización LU en lugar de la multiplicación de matriz-vector. En lugar de escribir el código C a mano, sería mejor usar un script para generarlo. También puede ser divertido ver si hay una diferencia de rendimiento apreciable al reordenar algunas de las operaciones para aprovechar mejor la canalización de instrucciones.También puede obtener algo de kilometraje de la herramienta STOKE , que explorará al azar el espacio de posibles transformaciones del programa para encontrar una versión más rápida.
fuente
Otra idea podría ser utilizar un enfoque generativo (un programa que escribe un programa). Cree un (meta) programa que escupe la secuencia de instrucciones C / C ++ para realizar LU ** sin pivotar en un sistema de 10x10 ... básicamente tomando el nido de bucle k / i / j y aplanándolo en O (1000) más o menos líneas de aritmética escalar Luego alimente ese programa generado en cualquier compilador de optimización. Lo que creo que es algo interesante aquí, es que eliminar los bucles expone cada dependencia de datos y subexpresión redundante, y le da al compilador la máxima oportunidad de reordenar las instrucciones para que se asignen bien al hardware real (por ejemplo, número de unidades de ejecución, riesgos / paradas, por lo que en).
Si conoce todas las matrices (o incluso solo algunas de ellas), puede mejorar el rendimiento llamando a funciones / intrínsecas SIMD (SSE / AVX) en lugar de código escalar. Aquí estaría explotando el vergonzoso paralelismo entre las instancias, en lugar de perseguir cualquier paralelismo dentro de una sola instancia. Por ejemplo, podría realizar 4 LU de doble precisión simultáneamente utilizando intrínsecos AVX256, al empacar 4 matrices "a través" del registro y realizar las mismas operaciones ** en todas ellas.
** De ahí el enfoque en LU sin pivotar. Pivotar arruina este enfoque de dos maneras. Primero, introduce ramas debido a la selección dinámica, lo que significa que sus dependencias de datos no son tan perfectamente conocidas. En segundo lugar, significa que diferentes "ranuras" SIMD tendrían que hacer cosas diferentes, porque la instancia A podría pivotar de manera diferente a la instancia B. Por lo tanto, si persigue algo de esto, sugeriría pivotar estáticamente sus matrices antes del cálculo (permuta la entrada más grande) de cada columna a diagonal).
fuente
Su pregunta lleva a dos consideraciones diferentes.
Primero, debe elegir el algoritmo correcto. Por lo tanto, se debe considerar la cuestión de si las matrices tienen alguna estructura. Por ejemplo, cuando las matrices son simétricas, una descomposición de Cholesky es más eficiente que LU. Cuando solo necesita una cantidad limitada de precisión, un método iterativo puede ser más rápido.
En total, la respuesta a su pregunta depende en gran medida del hardware y las matrices que considere. Probablemente no haya una respuesta definitiva y tenga que probar algunas cosas para encontrar un método óptimo.
fuente
Intentaría la inversión en bloque.
https://en.wikipedia.org/wiki/Invertible_matrix#Blockwise_inversion
Eigen utiliza una rutina optimizada para calcular el inverso de una matriz 4x4, que probablemente sea lo mejor que obtendrá. Intenta usar eso tanto como sea posible.
http://www.eigen.tuxfamily.org/dox/Inverse__SSE_8h_source.html
Arriba a la izquierda: 8x8. Arriba a la derecha: 8x2. Abajo a la izquierda: 2x8. Abajo a la derecha: 2x2. Invierta el 8x8 utilizando el código de inversión 4x4 optimizado. El resto son productos matriciales.
EDITAR: Usar bloques 6x6, 6x4, 4x6 y 4x4 ha demostrado ser un poco más rápido de lo que describí anteriormente.
Aquí están los resultados de una ejecución de referencia utilizando un millón de
Eigen::Matrix<double,10,10>::Random()
matrices yEigen::Matrix<double,10,1>::Random()
vectores. En todas mis pruebas, mi inverso es siempre más rápido. Mi rutina de resolución implica calcular el inverso y luego multiplicarlo por un vector. A veces es más rápido que Eigen, a veces no. Mi método de marcado de banco puede ser defectuoso (no desactivó el turbo boost, etc.). Además, las funciones aleatorias de Eigen pueden no ser representativas de datos reales.Estoy muy interesado en ver si alguien puede optimizar esto aún más, ya que tengo una aplicación de elementos finitos que invierte un millón de matrices de 10x10 (y sí, necesito coeficientes individuales de la inversa, por lo que resolver directamente un sistema lineal no siempre es una opción) .
fuente