ASCII数据导入:如何在C++中匹配Fortran的批量读取性能?

twi*_*nco 17 c++ ascii fortran

设置

您好,我有用于读取ASCII双精度数据的Fortran代码(问题底部的数据文件示例):

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
Run Code Online (Sandbox Code Playgroud)

和"等效的"C++代码:

#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;
}
Run Code Online (Sandbox Code Playgroud)

Fortran踢@ $$并取名字

注意这一行

read(11,*) charge
Run Code Online (Sandbox Code Playgroud)

执行与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];
Run Code Online (Sandbox Code Playgroud)

where InFile是一个if stream对象(请注意,虽然Fortran代码中的迭代器从1开始而不是0,但范围是相同的).

然而,Fortran代码管理办法,比C++代码的方式更快,我想是因为Fortran语言做一些聪明像读/根据范围和形状解析文件(的值mx,my,mz)都一气呵成,然后简单地指向charge到数据被读取的内存.相比之下,C++代码需要在每次迭代时来回访问InFile(然后charge通常很大),从而导致(我相信)更多的IO和内存操作.

我正在阅读潜在的数十亿个值(几千兆字节),所以我真的想要最大化性能.

我的问题:

如何在C++中实现Fortran代码的性能?

继续...

这是一个比上面的C++实现更快的(比上面的C++)C++实现,其中一个文件被读入一个char数组,然后chargechar解析数组时填充:

#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;
}
Run Code Online (Sandbox Code Playgroud)

同样,这比简单>>的基于运算符的方法快得多,但仍然比Fortran版本慢得多 - 更不用说更多代码了.

如何获得更好的表现?

我确信方法2是我自己实现的方法,但我很好奇我如何提高性能以匹配Fortran代码.我正在考虑和正在研究的事物类型是:

  • C++ 11和C++ 14的功能
  • 优化的C或C++库只做这种事情
  • 对方法2中使用的各种方法的改进

C++ String Toolkit

特别是,C++ String Toolkit库将采用FileData分隔符" \n"并给我一个字符串标记对象(调用它FileTokens,然后三重for循环看起来像

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();
Run Code Online (Sandbox Code Playgroud)

这会略微简化代码,但是在复制(本质上)FileDatainto 的内容方面还有额外的工作FileTokens,这可能会因使用该nextFloatToken()方法而失去任何性能提升(假定比strtok()/ atof()组合更有效).

C++ String Toolkit(StrTk)Tokenizer教程页面(包含在问题的底部)中有一个示例,它使用的是StrTk的for_each_line()处理器,它看起来与我想要的应用程序类似.然而,这些情况之间的区别在于我不能假设输入文件的每一行上会出现多少数据,而我对StrTk的了解不足以说明这是否是一个可行的解决方案.

不是重复

之前已经出现了将ASCII数据快速读取到数组或结构的主题,但我已经回顾了以下帖子并且他们的解决方案还不够:

示例数据

这是我正在导入的数据文件的示例.ASCII数据由空格和换行符分隔,如下例所示:

 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
Run Code Online (Sandbox Code Playgroud)

StrTk的例子

这是上面提到的StrTk示例.该方案是解析包含3D网格信息的数据文件:

输入数据:

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
Run Code Online (Sandbox Code Playgroud)

码:

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;
}
Run Code Online (Sandbox Code Playgroud)

Ton*_*roy 6

这个...

vector<vector<vector<double> > > charge(mx, vector<vector<double> >(my, vector<double>(mz)));
Run Code Online (Sandbox Code Playgroud)

...创建一个vector<double>(mz)具有全部0.0值的临时值,并将其复制my一次(或者可能移动然后my-1使用C++ 11编译器复制时间,但差别很小......)来创建一个临时的vector<vector<double>>(my, ...),然后复制的mx时间(... .as以上...)初始化所有数据.无论如何,你正在阅读这些元素的数据 - 没有必要花时间在这里初始化它.相反,创建一个空的charge并使用嵌套循环来reserve()为元素提供足够的内存,而不用填充它们.

接下来,检查您是否正在编译优化.如果你是,并且你仍然比FORTRAN慢,那么在数据填充嵌套循环中尝试创建对你所关注.emplace_back元素的向量的引用:

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);
        }
    }
Run Code Online (Sandbox Code Playgroud)

如果你的优化者做得很好,这应该没有用,但值得尝试作为一个健全性检查.

如果你仍然较慢 - 或者只是想要更快 - 你可以尝试优化你的数字解析:你说你的数据都是格式化的ala 0.23080516813E+04- 具有固定的大小,你可以很容易地计算读入缓冲区的字节数从内存中给你一个相当数量的值,然后每个你可以atol.提取23080516813之后开始,然后将它乘以10减去幂(11(你的位数)减去04):为了速度,保持一个使用提取的指数(即4)将这些权力的表格用10表示.(注意乘以例如1E-7可能比在许多常见硬件上除以1E7更快.)

如果你想闪现这个东西,切换到使用内存映射文件访问.值得考虑boost::mapped_file_source因为它比POSIX API(更不用说Windows)更容易使用,并且可移植,但是直接针对OS API进行编程也不应该是一个很大的困难.

更新 - 对第一和第二条评论的回复

使用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);
Run Code Online (Sandbox Code Playgroud)

上面将文件映射到地址处的内存中p,然后从第一行解析维度.继续解析以后的实际double表示++nl.我提到了上面的方法,你担心数据格式的变化:你可以在文件中添加一个版本号,这样你就可以使用优化的解析,直到版本号发生变化,然后再回到"未知"的通用版本文件格式.对于通用的东西,对于内存中的表示使用int chars_to_skip; double my_double; ASSERT(sscanf(ptr, "%f%n", &my_double, &chars_to_skip) == 1);是合理的:请参阅sscanf此处的文档 - 然后您可以通过数据推进指针chars_to_skip.

接下来,您是否建议将reserve()解决方案与参考创建解决方案相结合?

是.

而且(原谅我的无知)为什么会使用引用charge[i][j]并且v.emplace_back()比它更好charge[i][j].emplace_back()

这个建议是为了理智检查编译器没有重复评估charge[i][j]每个被放置的元素:希望它不会产生任何性能差异,你可以回到charge[i][j].emplace(),但恕我直言,值得快速检查.

最后,我怀疑在每个循环的顶部使用空向量和reserve().我有另一个程序,使用该方法停止研磨,并用预分配的多维向量替换reserve()s加速了很多.

这是可能的,但不是一般的或适用的位置不一定正确-很多依赖于编译器/优化器(特别是循环展开)等.随着未优化emplace_back你不必检查矢量size()反对capacity()反复,但如果优化器做得很好的是应该减少到无足轻重.与大量的性能调优一样,您通常无法完美地推理事物并得出最快的结论,并且必须尝试替代方案并使用您的实际编译器,程序数据等来测量它们.