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

时间:2015-01-22 05:36:07

标签: c++ fortran ascii

设置

您好,我有用于读取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

和“等效”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;
}

Fortran踢@ $$并取名

注意该行

read(11,*) charge

执行与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];

其中InFileif stream对象(请注意,虽然Fortran代码中的迭代器从1开始而不是0,但范围是相同的。)

然而,Fortran代码运行方式比C ++代码更快,我认为因为Fortran做了一些聪明的事情,比如根据范围和形状读取/解析文件(mx,{{1}的值},my)一气呵成,然后简单地将mz指向数据被读取的内存。相比之下,C ++代码需要在每次迭代时来回访问charge然后InFile(通常很大),从而导致(我相信)更多的IO和内存操作。

我正在阅读数十亿的价值(几千兆字节),所以我真的希望最大限度地提高性能。

我的问题:

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

继续......

这是一个比上述C ++实现快得多的C ++实现,其中文件一次读入charge数组,然后char填充为charge解析数组:

char

同样,这比基于运算符的简单#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; } 方法要快得多,但仍比Fortran版本慢得多 - 更不用说更多的代码了。

如何获得更好的表现?

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

  • C ++ 11和C ++ 14功能
  • 优化的C或C ++库,用于执行此类事情
  • 对方法2中使用的各个方法的改进

C ++ String Toolkit

特别是,C ++ String Toolkit库将使用atof()和分隔符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(); 的内容复制(实质上)到FileData还有额外的工作,这可能会因使用FileTokens而导致任何性能提升。方法(假定效率高于nextFloatToken() / strtok()组合)。

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

不是DUPLICATE

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

示例数据

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

for_each_line()

StrTk示例

以下是上面提到的StrTk example。该方案是解析包含3D网格信息的数据文件:

输入数据:

 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

代码:

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

1 个答案:

答案 0 :(得分:6)

此...

vector<vector<vector<double> > > charge(mx, vector<vector<double> >(my, vector<double>(mz)));

...使用所有0.0值创建一个临时vector<double>(mz),并将其复制my次(或者可能移动然后使用C ++ 11编译器复制my-1次,但是差别很小......)创建一个临时的vector<vector<double>>(my, ...),然后复制mx次(......如上所述......)来初始化所有数据。无论如何,你正在阅读这些元素的数据 - 没有必要花时间在这里初始化它。相反,创建一个空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);
        }
    }

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

如果你仍然比较慢 - 或者只是想要更快 - 你可以尝试优化你的数字解析:你说你的数据都是格式化的0.23080516813E+04 - 具有固定的大小,你可以很容易地计算出来读入缓冲区需要多少字节才能从内存中获取相当数量的值,然后对于每一个,你可以在atol之后启动.以提取23080516813,然后将其乘以10到10的幂减去(11(你的位数)减去04):对于速度,保留一个10的幂的表并使用提取的指数(即4)将其索引到其中。 (注意乘以例如1E-7可能比在许多常见硬件上除以1E7更快。)

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

更新 - 对第一个和第一个的响应第二条评论

使用增强内存映射的示例:

#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);

以上将文件映射到地址p的内存中,然后从第一行解析维度。继续从double开始解析实际的++nl表示。我提到了上面的方法,你担心数据格式的变化:你可以在文件中添加一个版本号,这样你就可以使用优化的解析,直到版本号发生变化,然后再回到“未知”的通用版本文件格式。就通用的内容而言,使用int chars_to_skip; double my_double; ASSERT(sscanf(ptr, "%f%n", &my_double, &chars_to_skip) == 1);的内存中表示是合理的:请参阅sscanf docs here - 然后您可以通过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,你必须检查向量{{1}反复地反对size(),但如果优化者做得好,应该减少到无足轻重。与大量的性能调优一样,您通常无法完美地推断事物并得出最快的结论,并且必须尝试替代方案并使用您的实际编译器,程序数据等来测量它们。