Importación de datos ASCII: ¿cómo puedo igualar el rendimiento de lectura masiva de Fortran en C++?
(1)
Esta...
vector<vector<vector<double> > > charge(mx, vector<vector<double> >(my, vector<double>(mz)));
... crea un vector<double>(mz)
temporal vector<double>(mz)
, con todos los valores de 0.0, y lo copia my
tiempos (o tal vez mueve luego my-1
veces con un compilador de C ++ 11, pero poca diferencia ...) a cree un vector<vector<double>>(my, ...)
temporal vector<vector<double>>(my, ...)
que luego se copiará mx
veces (... como arriba ...) para inicializar todos los datos. De todos modos, está leyendo datos sobre estos elementos: no es necesario que dedique tiempo a inicializarlos aquí. En su lugar, cree una charge
vacía y use bucles anidados para reserve()
suficiente memoria para los elementos sin que todavía los haya completado.
A continuación, compruebe que está compilando con la optimización en. Si es y sigue siendo más lento que FORTRAN, en los bucles anidados que .emplace_back
los datos, intente crear una referencia al vector en el que se .emplace_back
elementos .emplace_back
para:
for (int i = 0; i < mx; ++i)
for (int j = 0; j < my; ++j)
{
std::vector<double>& v = charge[i][j];
for (int k = 0; k < mz; ++k)
{
double d;
InFile >> d;
v.emplace_pack(d);
}
}
Eso no debería ayudar si su optimizador ha hecho un buen trabajo, pero vale la pena intentarlo como una prueba de cordura.
Si aún es más lento, o simplemente quiere intentar ser aún más rápido, podría intentar optimizar su análisis de números: dice que todos sus datos tienen el formato de ala 0.23080516813E+04
- con tamaños fijos como que puede calcular fácilmente cuántos bytes lea en un búfer para darle un número decente de valores de la memoria, luego para cada uno podría comenzar un atol
después de .
para extraer 23080516813 luego multiplíquelo por 10 a la potencia de menos (11 (su número de dígitos) menos 04): para velocidad, mantenga una tabla de esas potencias de diez e indexe usando el exponente extraído (es decir, 4). (La multiplicación de notas por ejemplo, 1E-7 puede ser más rápida que la división por 1E7 en una gran cantidad de hardware común).
Y si desea bombardear esto, cambie al uso del acceso a archivos asignados en la memoria. Vale la pena considerar boost::mapped_file_source
ya que es más fácil de usar que incluso la API POSIX (por no hablar de Windows) y portátil, pero la programación directamente contra una API del sistema operativo tampoco debería ser una gran dificultad.
ACTUALIZACIÓN - respuesta a primer y segundo comentario
Ejemplo de uso de la asignación de memoria boost:
#include <boost/iostreams/device/mapped_file.hpp>
boost::mapped_file_params params("dbldat.in");
boost::mapped_file_source file(params);
file.open();
ASSERT(file.is_open());
const char* p = file.data();
const char* nl = strchr(p, ''/n'');
std::istringstream iss(std::string(p, nl - p));
size_t x, y, z;
ASSERT(iss >> x >> y >> z);
Lo anterior asigna un archivo a la memoria en la dirección p
, luego analiza las dimensiones de la primera línea. Continúe analizando las representaciones double
reales desde ++nl
adelante. Menciono un enfoque de lo anterior, y le preocupa el cambio de formato de los datos: puede agregar un número de versión al archivo, por lo que puede usar el análisis optimizado hasta que el número de versión cambie y luego recurra a algo genérico para "desconocido" formatos de archivo. En cuanto a algo genérico, para las representaciones en memoria usando int chars_to_skip; double my_double; ASSERT(sscanf(ptr, "%f%n", &my_double, &chars_to_skip) == 1);
int chars_to_skip; double my_double; ASSERT(sscanf(ptr, "%f%n", &my_double, &chars_to_skip) == 1);
es razonable: vea sscanf
docs aquí - luego puede avanzar el puntero a través de los datos con chars_to_skip
.
A continuación, ¿está sugiriendo combinar la solución
reserve()
con la solución de creación de referencia?
Sí.
Y (perdón por mi ignorancia) ¿por qué sería mejor usar una referencia para
charge[i][j]
yv.emplace_back()
quecharge[i][j].emplace_back()
?
Esa sugerencia fue para verificar que el compilador no evalúa repetidamente la charge[i][j]
para cada elemento que se está colocando: es de esperar que no haya diferencia de rendimiento y que pueda volver a la charge[i][j].emplace()
, pero en mi humilde opinión vale la pena un control rápido.
Por último, soy escéptico sobre el uso de un vector vacío y la reserva () en la parte superior de cada bucle. Tengo otro programa que se detuvo con el uso de ese método y la sustitución de las reservas () con un vector multidimensional preasignado lo aceleró mucho.
Eso es posible, pero no necesariamente cierto en general o aplicable aquí: mucho depende del compilador / optimizador (particularmente el desenrollado de bucles), etc. Con emplace_back no emplace_back
, tiene que verificar el size()
vector size()
contra la capacity()
repetidamente, pero si el optimizador hace un buen trabajo que debe reducirse a la insignificancia. Al igual que con muchos ajustes de rendimiento, a menudo no puede razonar sobre las cosas a la perfección y concluir lo que va a ser más rápido, y tendrá que probar alternativas y medirlas con su compilador real, datos de programas, etc.
La puesta en marcha
Hola, tengo el código de Fortran para leer en datos de doble precisión ASCII (ejemplo de archivo de datos al final de la pregunta):
program ReadData
integer :: mx,my,mz
doubleprecision, allocatable, dimension(:,:,:) :: charge
! Open the file ''CHGCAR''
open(11,file=''CHGCAR'',status=''old'')
! Get the extent of the 3D system and allocate the 3D array
read(11,*)mx,my,mz
allocate(charge(mx,my,mz) )
! Bulk read the entire block of ASCII data for the system
read(11,*) charge
end program ReadData
y el código C ++ "equivalente":
#include <fstream>
#include <vector>
using std::ifstream;
using std::vector;
using std::ios;
int main(){
int mx, my, mz;
// Open the file ''CHGCAR''
ifstream InFile(''CHGCAR'', ios::in);
// Get the extent of the 3D system and allocate the 3D array
InFile >> mx >> my >> mz;
vector<vector<vector<double> > > charge(mx, vector<vector<double> >(my, vector<double>(mz)));
// Method 1: std::ifstream extraction operator to double
for (int i = 0; i < mx; ++i)
for (int j = 0; j < my; ++j)
for (int k = 0; k < mz; ++k)
InFile >> charge[i][j][k];
return 0;
}
Fortran pateando @ $$ y tomando nombres.
Tenga en cuenta que la línea
read(11,*) charge
realiza la misma tarea que el código C ++:
for (int i = 0; i < mx; ++i)
for (int j = 0; j < my; ++j)
for (int k = 0; k < mz; ++k)
InFile >> charge[i][j][k];
donde InFile
es un objeto if stream
(tenga en cuenta que, si bien los iteradores en el código Fortran comienzan en 1 y no en 0, el rango es el mismo).
Sin embargo, el código de Fortran se ejecuta mucho, más rápido que el código de C ++, creo que porque Fortran hace algo inteligente como leer / analizar el archivo de acuerdo con el rango y la forma (valores de mx
, my
, mz
) todo de una vez, y luego simplemente apuntando la charge
a la memoria donde se leen los datos. El código C ++, en comparación, necesita acceder a InFile
y luego charge
(que suele ser grande) de ida y vuelta con cada iteración, lo que da como resultado (creo) muchas más operaciones de E / S y memoria.
Estoy leyendo potencialmente miles de millones de valores (varios gigabytes), así que realmente quiero maximizar el rendimiento.
Mi pregunta:
¿Cómo puedo lograr el rendimiento del código Fortran en C ++?
Continuando ...
Aquí hay una implementación de C ++ mucho más rápida (que la anterior en C ++), donde el archivo se lee de una sola vez en una matriz de caracteres, y luego se rellena el charge
medida que se analiza la matriz de caracteres:
#include <fstream>
#include <vector>
#include <cstdlib>
using std::ifstream;
using std::vector;
using std::ios;
int main(){
int mx, my, mz;
// Open the file ''CHGCAR''
ifstream InFile(''CHGCAR'', ios::in);
// Get the extent of the 3D system and allocate the 3D array
InFile >> mx >> my >> mz;
vector<vector<vector<double> > > charge(mx, vector<vector<double> >(my, vector<double>(mz)));
// Method 2: big char array with strtok() and atof()
// Get file size
InFile.seekg(0, InFile.end);
int FileSize = InFile.tellg();
InFile.seekg(0, InFile.beg);
// Read in entire file to FileData
vector<char> FileData(FileSize);
InFile.read(FileData.data(), FileSize);
InFile.close();
/*
* Now simply parse through the char array, saving each
* value to its place in the array of charge density
*/
char* TmpCStr = strtok(FileData.data(), " /n");
// Gets TmpCStr to the first data value
for (int i = 0; i < 3 && TmpCStr != NULL; ++i)
TmpCStr = strtok(NULL, " /n");
for (int i = 0; i < Mz; ++i)
for (int j = 0; j < My; ++j)
for (int k = 0; k < Mx && TmpCStr != NULL; ++k){
Charge[i][j][k] = atof(TmpCStr);
TmpCStr = strtok(NULL, " /n");
}
return 0;
}
Nuevamente, esto es mucho más rápido que el método simple >>
basado en el operador, pero aún considerablemente más lento que la versión Fortran, por no mencionar mucho más código.
¿Cómo obtener un mejor rendimiento?
Estoy seguro de que el método 2 es el camino a seguir si voy a implementarlo yo mismo, pero tengo curiosidad por saber cómo puedo aumentar el rendimiento para que coincida con el código Fortran. Los tipos de cosas que estoy considerando y que estoy investigando actualmente son:
- Características de C ++ 11 y C ++ 14
- Biblioteca C o C ++ optimizada para hacer este tipo de cosas
- Mejoras en los métodos individuales utilizados en el método 2
- biblioteca de tokenización como la de la biblioteca de herramientas de cadenas de C ++ en lugar de
strtok()
-
char
más eficiente paradouble
conversión queatof()
- biblioteca de tokenización como la de la biblioteca de herramientas de cadenas de C ++ en lugar de
Kit de herramientas de cadena C ++
En particular, la biblioteca del kit de herramientas de cadenas de C ++ tomará FileData
y los delimitadores " /n"
y me dará un objeto de token de cadena ( FileTokens
, entonces el triple for
bucle se vería como
for (int k = 0; k < Mz; ++k)
for (int j = 0; j < My; ++j)
for (int i = 0; i < Mx; ++i)
Charge[k][j][i] = FileTokens.nextFloatToken();
Esto simplificaría un poco el código, pero hay un trabajo adicional al copiar (en esencia) el contenido de FileData
en FileTokens
, lo que podría eliminar cualquier ganancia de rendimiento al usar el método nextFloatToken()
(presumiblemente más eficiente que el strtok()
/ atof()
combinación).
Hay un ejemplo en la página del tutorial del Tokenizer del String Toolkit (StrTk) (incluido en la parte inferior de la pregunta) utilizando el procesador for_each_line()
de for_each_line()
que parece ser similar a mi aplicación deseada. Sin embargo, una diferencia entre los casos es que no puedo asumir cuántos datos aparecerán en cada línea del archivo de entrada, y no sé lo suficiente sobre StrTk para decir si esta es una solución viable.
NO ES UN DUPLICADO
El tema de la lectura rápida de datos ASCII en una matriz o estructura ha surgido anteriormente, pero he revisado las siguientes publicaciones y sus soluciones no fueron suficientes:
- La forma más rápida de leer datos de muchos archivos ASCII
- Cómo leer los números de un archivo ASCII (C ++)
- Leer datos numéricos de un archivo de texto en C ++
- Leyendo un archivo y almacenando los contenidos en una matriz.
- C / C ++ Lectura rápida de un gran archivo de datos ASCII a una matriz o estructura
- Lea el archivo ASCII en la matriz en C ++
- ¿Cómo puedo leer el archivo de datos ASCII en C ++?
- Leyendo un archivo y almacenando los contenidos en una matriz.
- Lectura de datos en columnas de un archivo (C ++)
- La forma más rápida de leer un archivo .txt
- ¿Cómo funciona la entrada / salida rápida en C / C ++, mediante el uso de registros, números hexadecimales y me gusta?
- leyendo el archivo en la estructura struct
Ejemplo de datos
Aquí hay un ejemplo del archivo de datos que estoy importando. Los datos ASCII están delimitados por espacios y saltos de línea como el siguiente ejemplo:
5 3 3
0.23080516813E+04 0.22712439791E+04 0.21616898980E+04 0.19829996749E+04 0.17438686650E+04
0.14601734127E+04 0.11551623512E+04 0.85678544224E+03 0.59238325489E+03 0.38232265554E+03
0.23514479113E+03 0.14651943589E+03 0.10252743482E+03 0.85927499703E+02 0.86525872161E+02
0.10141182750E+03 0.13113419142E+03 0.18057147781E+03 0.25973252462E+03 0.38303754418E+03
0.57142097675E+03 0.85963728360E+03 0.12548019843E+04 0.17106124085E+04 0.21415379433E+04
0.24687336309E+04 0.26588012477E+04 0.27189091499E+04 0.26588012477E+04 0.24687336309E+04
0.21415379433E+04 0.17106124085E+04 0.12548019843E+04 0.85963728360E+03 0.57142097675E+03
0.38303754418E+03 0.25973252462E+03 0.18057147781E+03 0.13113419142E+03 0.10141182750E+03
0.86525872161E+02 0.85927499703E+02 0.10252743482E+03 0.14651943589E+03 0.23514479113E+03
Ejemplo de StrTk
Aquí está el ejemplo de StrTk mencionado anteriormente. El escenario es analizar el archivo de datos que contiene la información para una malla 3D:
datos de entrada:
5
+1.0,+1.0,+1.0
-1.0,+1.0,-1.0
-1.0,-1.0,+1.0
+1.0,-1.0,-1.0
+0.0,+0.0,+0.0
4
0,1,4
1,2,4
2,3,4
3,1,4
código:
struct point
{
double x,y,z;
};
struct triangle
{
std::size_t i0,i1,i2;
};
int main()
{
std::string mesh_file = "mesh.txt";
std::ifstream stream(mesh_file.c_str());
std::string s;
// Process points section
std::deque<point> points;
point p;
std::size_t point_count = 0;
strtk::parse_line(stream," ",point_count);
strtk::for_each_line_n(stream,
point_count,
[&points,&p](const std::string& line)
{
if (strtk::parse(line,",",p.x,p.y,p.z))
points.push_back(p);
});
// Process triangles section
std::deque<triangle> triangles;
triangle t;
std::size_t triangle_count = 0;
strtk::parse_line(stream," ",triangle_count);
strtk::for_each_line_n(stream,
triangle_count,
[&triangles,&t](const std::string& line)
{
if (strtk::parse(line,",",t.i0,t.i1,t.i2))
triangles.push_back(t);
});
return 0;
}