¿Es una buena idea usar el vector <vector <double>> para formar una clase de matriz para el código informático científico de alto rendimiento?
37
¿Es una buena idea usar vector<vector<double>>(usando std) para formar una clase de matriz para el código de computación científica de alto rendimiento?
-1 Por supuesto que es una mala idea. No podrá utilizar blas, lapack ni ninguna otra biblioteca matricial existente con dicho formato de almacenamiento. Además, introduce ineficiencias por datos no locales e indirectos
Thomas Klimpel
99
@Thomas ¿Eso realmente justifica un voto negativo?
akid
33
No desestimes. Es una pregunta legítima, incluso si es una idea equivocada.
Wolfgang Bangerth
3
std :: vector no es un vector distribuido, por lo que no podrá hacer mucha computación paralela con él (a excepción de las máquinas de memoria compartida), use Petsc o Trilinos en su lugar. Además, uno generalmente trata con matrices dispersas y estaría almacenando matrices densas completas. Para jugar con matrices dispersas, podría usar un std :: vector <std :: map> pero nuevamente, esto no funcionaría muy bien, vea la publicación @WolfgangBangerth a continuación.
gnzlbg
3
intente usar std :: vector <std :: vector <double>> con MPI y querrá dispararse a sí mismo
pyCthon
Respuestas:
43
Es una mala idea porque el vector necesita asignar tantos objetos en el espacio como filas en su matriz. La asignación es costosa, pero principalmente es una mala idea porque los datos de su matriz ahora existen en una serie de arreglos dispersos por la memoria, en lugar de todos en un lugar donde el caché del procesador puede acceder fácilmente.
También es un formato de almacenamiento derrochador: std :: vector almacena dos punteros, uno al principio de la matriz y otro al final porque la longitud de la matriz es flexible. Por otro lado, para que esta sea una matriz adecuada, las longitudes de todas las filas deben ser las mismas y, por lo tanto, sería suficiente almacenar el número de columnas solo una vez, en lugar de permitir que cada fila almacene su longitud de forma independiente.
En realidad, es peor de lo que dices, porque en std::vectorrealidad almacena tres punteros: el comienzo, el final y el final de la región de almacenamiento asignada (lo que nos permite llamar, por ejemplo .capacity()). ¡Esa capacidad puede ser diferente al tamaño hace que la situación sea mucho peor!
user14717
18
Además de las razones que mencionó Wolfgang, si usa un vector<vector<double> >, deberá desreferenciarlo dos veces cada vez que desee recuperar un elemento, que es más costoso desde el punto de vista computacional que una sola operación de desreferenciación. Un enfoque típico es asignar una matriz única (ao vector<double>a double *) en su lugar. También he visto a personas agregar azúcar sintáctico a las clases de matriz al envolver alrededor de esta matriz única algunas operaciones de indexación más intuitivas, para reducir la cantidad de "sobrecarga mental" necesaria para invocar los índices adecuados.
@Wolfgang: Dependiendo del tamaño de la matriz densa, dos punteros adicionales por fila pueden ser insignificantes. Con respecto a los datos dispersos, se podría pensar en usar un asignador personalizado que se asegure de que los vectores estén en memoria contigua. Siempre que la memoria no se recicle, incluso el asignador estándar utilizará memoria contigua con un espacio de dos punteros.
@Geoff: si está haciendo acceso aleatorio y usa solo una matriz, aún tiene que calcular el índice. Puede que no sea más rápido.
En mi sistema ahora hay un claro ganador (Compilador gcc 4.7 con -O3)
impresiones de vectormatrix de tiempo:
index 997:3
index 998:3
index 999:30xc7fc680xc7fc80
calc took:185.507 k=100000000
real 0m0.257s
user 0m0.244s
sys 0m0.008s
También vemos que, mientras el asignador estándar no recicle la memoria liberada, los datos son contiguos. (Por supuesto, después de algunas desasignaciones no hay garantía para esto).
impresiones de matriz de tiempo:
index 997:1
index 998:1
index 999:10x7ff41f208f480x7ff41f208f50
calc took:187.349 k=100000000
real 0m0.257s
user 0m0.248s
sys 0m0.004s
Usted escribe "En mi sistema ahora hay un claro ganador".
akid
99
-1 Comprender el rendimiento del código hpc puede no ser trivial. En su caso, el tamaño de la matriz simplemente excede el tamaño de la memoria caché, por lo que solo está midiendo el ancho de banda de memoria de su sistema. Si cambio N a 200 y aumento el número de iteraciones a 1000, obtengo "calc tomó: 65" frente a "calc tomó: 36". Si reemplazo más a = a * a por a + = a1 * a2 para hacerlo más realista, obtengo "calc tome: 176" vs "calc tomó: 84". Por lo tanto, parece que puede perder un factor dos en el rendimiento utilizando un vector de vectores en lugar de una matriz. La vida real será más complicada, pero sigue siendo una mala idea.
Thomas Klimpel
sí, pero intente usar std :: vectors con MPI, C gana sin
dudas
4
No lo recomiendo, pero no por problemas de rendimiento. Será un poco menos eficaz que una matriz tradicional, que generalmente se asigna como una gran porción de datos contiguos que se indexan utilizando una única referencia de puntero y aritmética de enteros. La razón del impacto en el rendimiento es principalmente las diferencias de almacenamiento en caché, pero una vez que el tamaño de su matriz sea lo suficientemente grande, este efecto se amortizará y si usa un asignador especial para los vectores internos para que estén alineados con los límites del caché, esto mitiga aún más el problema de almacenamiento en caché .
Eso en sí mismo no es razón suficiente para no hacerlo, en mi opinión. La razón para mí es que crea muchos dolores de cabeza de codificación. Aquí hay una lista de dolores de cabeza que esto causará a largo plazo
Uso de bibliotecas HPC
Si desea utilizar la mayoría de las bibliotecas de HPC, deberá iterar sobre su vector y colocar todos sus datos en un búfer contiguo, porque la mayoría de las bibliotecas de HPC esperan este formato explícito. Me vienen a la mente BLAS y LAPACK, pero también la ubicua biblioteca HPC MPI sería mucho más difícil de usar.
Más potencial para codificar errores
std::vectorno sabe nada de sus entradas. Si llena una std::vectorcon más std::vectors, entonces es completamente su trabajo asegurarse de que todas tengan el mismo tamaño, porque recuerde que queremos una matriz y las matrices no tienen un número variable de filas (o columnas). Por lo tanto, tendrá que llamar a todos los constructores correctos para cada entrada de su vector externo, y cualquier otra persona que use su código debe resistir la tentación de usar std::vector<T>::push_back()cualquiera de los vectores internos, lo que provocaría que se rompa todo el código siguiente. Por supuesto, puede rechazar esto si escribe su clase correctamente, pero es mucho más fácil aplicar esto simplemente con una gran asignación contigua.
Cultura y expectativas de HPC
Los programadores de HPC simplemente esperan datos de bajo nivel. Si les da una matriz, existe la expectativa de que si agarran el puntero al primer elemento de la matriz y un puntero al último elemento de la matriz, todos los punteros entre estos dos son válidos y apuntan a elementos de ese mismo matriz. Esto es similar a mi primer punto, pero diferente porque puede no estar relacionado tanto con las bibliotecas sino con los miembros del equipo o cualquier persona con la que comparta su código.
Es más fácil razonar sobre el rendimiento de los datos de nivel inferior
Bajar al nivel más bajo de representación de su estructura de datos deseada hace que su vida sea más fácil a largo plazo para HPC. El uso de herramientas como perfy vtunele proporcionará mediciones de contador de rendimiento de muy bajo nivel que intentará combinar con los resultados de creación de perfiles tradicionales para mejorar el rendimiento de su código. Si su estructura de datos utiliza muchos contenedores sofisticados, será difícil entender que las fallas de caché provienen de un problema con el contenedor o de una ineficiencia en el algoritmo mismo. Para los contenedores de código más complicados son necesarios, pero para el álgebra matricial realmente no lo son: puede arreglárselas solo 1std::vectorpara almacenar los datos en lugar de nstd::vectors, así que vaya con eso.
También escribo un punto de referencia. Para la matriz de tamaño pequeño (<100 * 100), el rendimiento es similar para el vector <vector <double >> y el vector 1D envuelto. Para una matriz de gran tamaño (~ 1000 * 1000), el vector 1D envuelto es mejor. La matriz de Eigen se comporta peor. Me sorprende que el Eigen sea el peor.
Como otros han señalado, no intentes hacer cálculos matemáticos con él ni hacer nada performante.
Dicho esto, he usado esta estructura como temporal cuando el código necesita ensamblar una matriz 2-D cuyas dimensiones se determinarán en tiempo de ejecución y después de que haya comenzado a almacenar datos. Por ejemplo, recopilar salidas de vectores de algún proceso costoso en el que no es simple calcular exactamente cuántos vectores necesitará almacenar al inicio.
Podrías concatenar todas tus entradas de vectores en un búfer a medida que entran, pero el código será más duradero y más legible si utilizas a vector<vector<T>>.
Respuestas:
Es una mala idea porque el vector necesita asignar tantos objetos en el espacio como filas en su matriz. La asignación es costosa, pero principalmente es una mala idea porque los datos de su matriz ahora existen en una serie de arreglos dispersos por la memoria, en lugar de todos en un lugar donde el caché del procesador puede acceder fácilmente.
También es un formato de almacenamiento derrochador: std :: vector almacena dos punteros, uno al principio de la matriz y otro al final porque la longitud de la matriz es flexible. Por otro lado, para que esta sea una matriz adecuada, las longitudes de todas las filas deben ser las mismas y, por lo tanto, sería suficiente almacenar el número de columnas solo una vez, en lugar de permitir que cada fila almacene su longitud de forma independiente.
fuente
std::vector
realidad almacena tres punteros: el comienzo, el final y el final de la región de almacenamiento asignada (lo que nos permite llamar, por ejemplo.capacity()
). ¡Esa capacidad puede ser diferente al tamaño hace que la situación sea mucho peor!Además de las razones que mencionó Wolfgang, si usa un
vector<vector<double> >
, deberá desreferenciarlo dos veces cada vez que desee recuperar un elemento, que es más costoso desde el punto de vista computacional que una sola operación de desreferenciación. Un enfoque típico es asignar una matriz única (aovector<double>
adouble *
) en su lugar. También he visto a personas agregar azúcar sintáctico a las clases de matriz al envolver alrededor de esta matriz única algunas operaciones de indexación más intuitivas, para reducir la cantidad de "sobrecarga mental" necesaria para invocar los índices adecuados.fuente
No, use una de las bibliotecas de álgebra lineal disponibles gratuitamente. Aquí se puede encontrar una discusión sobre las diferentes bibliotecas: ¿ Recomendaciones para una biblioteca de matriz C ++ rápida y utilizable?
fuente
¿Es realmente tan malo?
@Wolfgang: Dependiendo del tamaño de la matriz densa, dos punteros adicionales por fila pueden ser insignificantes. Con respecto a los datos dispersos, se podría pensar en usar un asignador personalizado que se asegure de que los vectores estén en memoria contigua. Siempre que la memoria no se recicle, incluso el asignador estándar utilizará memoria contigua con un espacio de dos punteros.
@Geoff: si está haciendo acceso aleatorio y usa solo una matriz, aún tiene que calcular el índice. Puede que no sea más rápido.
Entonces, hagamos una pequeña prueba:
vectormatrix.cc:
Y ahora usando una matriz:
arraymatrix.cc
En mi sistema ahora hay un claro ganador (Compilador gcc 4.7 con -O3)
impresiones de vectormatrix de tiempo:
También vemos que, mientras el asignador estándar no recicle la memoria liberada, los datos son contiguos. (Por supuesto, después de algunas desasignaciones no hay garantía para esto).
impresiones de matriz de tiempo:
fuente
No lo recomiendo, pero no por problemas de rendimiento. Será un poco menos eficaz que una matriz tradicional, que generalmente se asigna como una gran porción de datos contiguos que se indexan utilizando una única referencia de puntero y aritmética de enteros. La razón del impacto en el rendimiento es principalmente las diferencias de almacenamiento en caché, pero una vez que el tamaño de su matriz sea lo suficientemente grande, este efecto se amortizará y si usa un asignador especial para los vectores internos para que estén alineados con los límites del caché, esto mitiga aún más el problema de almacenamiento en caché .
Eso en sí mismo no es razón suficiente para no hacerlo, en mi opinión. La razón para mí es que crea muchos dolores de cabeza de codificación. Aquí hay una lista de dolores de cabeza que esto causará a largo plazo
Uso de bibliotecas HPC
Si desea utilizar la mayoría de las bibliotecas de HPC, deberá iterar sobre su vector y colocar todos sus datos en un búfer contiguo, porque la mayoría de las bibliotecas de HPC esperan este formato explícito. Me vienen a la mente BLAS y LAPACK, pero también la ubicua biblioteca HPC MPI sería mucho más difícil de usar.
Más potencial para codificar errores
std::vector
no sabe nada de sus entradas. Si llena unastd::vector
con másstd::vector
s, entonces es completamente su trabajo asegurarse de que todas tengan el mismo tamaño, porque recuerde que queremos una matriz y las matrices no tienen un número variable de filas (o columnas). Por lo tanto, tendrá que llamar a todos los constructores correctos para cada entrada de su vector externo, y cualquier otra persona que use su código debe resistir la tentación de usarstd::vector<T>::push_back()
cualquiera de los vectores internos, lo que provocaría que se rompa todo el código siguiente. Por supuesto, puede rechazar esto si escribe su clase correctamente, pero es mucho más fácil aplicar esto simplemente con una gran asignación contigua.Cultura y expectativas de HPC
Los programadores de HPC simplemente esperan datos de bajo nivel. Si les da una matriz, existe la expectativa de que si agarran el puntero al primer elemento de la matriz y un puntero al último elemento de la matriz, todos los punteros entre estos dos son válidos y apuntan a elementos de ese mismo matriz. Esto es similar a mi primer punto, pero diferente porque puede no estar relacionado tanto con las bibliotecas sino con los miembros del equipo o cualquier persona con la que comparta su código.
Es más fácil razonar sobre el rendimiento de los datos de nivel inferior
Bajar al nivel más bajo de representación de su estructura de datos deseada hace que su vida sea más fácil a largo plazo para HPC. El uso de herramientas como
perf
yvtune
le proporcionará mediciones de contador de rendimiento de muy bajo nivel que intentará combinar con los resultados de creación de perfiles tradicionales para mejorar el rendimiento de su código. Si su estructura de datos utiliza muchos contenedores sofisticados, será difícil entender que las fallas de caché provienen de un problema con el contenedor o de una ineficiencia en el algoritmo mismo. Para los contenedores de código más complicados son necesarios, pero para el álgebra matricial realmente no lo son: puede arreglárselas solo1
std::vector
para almacenar los datos en lugar den
std::vector
s, así que vaya con eso.fuente
También escribo un punto de referencia. Para la matriz de tamaño pequeño (<100 * 100), el rendimiento es similar para el vector <vector <double >> y el vector 1D envuelto. Para una matriz de gran tamaño (~ 1000 * 1000), el vector 1D envuelto es mejor. La matriz de Eigen se comporta peor. Me sorprende que el Eigen sea el peor.
fuente
Como otros han señalado, no intentes hacer cálculos matemáticos con él ni hacer nada performante.
Dicho esto, he usado esta estructura como temporal cuando el código necesita ensamblar una matriz 2-D cuyas dimensiones se determinarán en tiempo de ejecución y después de que haya comenzado a almacenar datos. Por ejemplo, recopilar salidas de vectores de algún proceso costoso en el que no es simple calcular exactamente cuántos vectores necesitará almacenar al inicio.
Podrías concatenar todas tus entradas de vectores en un búfer a medida que entran, pero el código será más duradero y más legible si utilizas a
vector<vector<T>>
.fuente