【问题标题】:the fastest way to parse large file (~2 Gb) delimited with Tab解析用 Tab 分隔的大文件 (~2 Gb) 的最快方法
【发布时间】:2019-01-01 20:23:54
【问题描述】:

请指导我:有没有更有效的方法来执行这些任务:

  1. 将第一个制表符分隔文件中的键值对读取到映射容器中
  2. 从第二个制表符分隔文件(键值对)读取时,写入第三个文件

为 fname1 和 fname2 执行这两个步骤需要 3700 秒,共 7,000,000 行。

#include <string>
#include <map>
#include <fstream>
string col1;
int x;
map<string, int> m;
ifstream is1(fname1, ios::in | ios::binary);
ifstream is2(fname2, ios::in | ios::binary);
ofstream os3(fname3, ios::out | ios::binary);
if (is1.is_open())
{
    while (is1 >> col1 >> x)
    {
        m[col1] = x;
    }
    is.close();
}
if (is2.is_open() && os3.is_open())
{
    while (is2 >> col1 >> x)
    {
        if (m.count(col1) > 0)
            x += m[col1];
        os3 << col1 << "\t" << x << endl;
    }
    is2.close();
    os3.close();
}

我做错了什么?有没有更有效的方法来执行这些任务? 还是文件 I/O 在大多数情况下是瓶颈?

更新:这里我放了相同算法的两个实现。主要问题:为什么 pythonic 版本的工作更快?我决定改用 C++,因为我听说它提供了更快的代码。我错了吗?

fname1, fname2 - 输入。 fname3 - 所需的输出。

fname1:

col1 col2 col3

1 1 1

2 2 2

fname2:

col1 col2 col3

1 1 2

3 3 3

fname3:

col1 col2 col3

1 1 3

2 2 2

3 3 3

def merge_two_files(fname1, fname2, fname3):
    fout=open(fname3,'w')
    fin1=open(fname1)
    d1=dict()
    for line in fin1:
        l=line.strip().split('\t')
        key='\t'.join(l[0:2])
        d1[key] = float(l[2])
    fin1.close()
    d2=dict()
    fin2=open(fname2)
    for line in fin2:
        l=line.strip().split('\t')
        key='\t'.join(l[0:2])
        d2[key] = float(l[2])
    fin2.close()
    for e in d1.viewkeys() & d2.viewkeys():
        line_out='\t'.join([e,'{:.2f}'.format(d1[e]+d2[e])])
        fout.write(line_out+'\n')
    for e in d1.viewkeys() - (d1.viewkeys() & d2.viewkeys())
        line_out='\t'.join([e,'{:.2f}'.format(d1[e])])
        fout.write(line_out+'\n')
    for e in d2.viewkeys() - (d1.viewkeys() & d2.viewkeys())
        line_out='\t'.join([e,'{:.2f}'.format(d2[e])])
        fout.write(line_out+'\n')

#include <fstream>
#include <string>
#include <unordered_map>
#include <set>

using namespace std;

int main() {
    unordered_map < string, float > map1, map2 ;
    set < string > s1, s2, both ;
    string col1, col2, key, fname1, fname2, fname3 ;
    float col3 ;
    ifstream f1 ( fname1, ios::in | ios::binary) ;
    ifstream f2 ( fname2, ios::in | ios::binary) ;
    ofstream f3 ( fname3, ios::out | ios::binary) ;

    if ( f1.is_open() ) {
        while ( f1 >> col1 >> col2 >> col3 )
        key= col1 + "\t" + col2 ;
        map1.insert(make_pair(key,col3)) ;
        s1.insert(key) ;
    }
    f1.close()
    if ( f2.is_open() ) {
        while ( f2 >> col1 >> col2 >> col3 ) {
        key= col1 + "\t" + col2 ;
        map2.insert(make_pair(key,col3)) ;
        s2.insert(key) ;
        }
    }
    f2.close() ;

    set_intersection(s1.begin(), s1.end(),
                     s2.begin(), s2.end(),
                     inserter(both, both.begin())) ;
    if ( f3.is_open() ) {
        for ( const auto& e : both ) {
            f3 << e << "\t" << map1.at(e) + map2.at(e) << "\n" ;
        }
        for ( const auto& kv : map1 ) {
            if ( both.count(kv.first) ) continue ;
            f3 << kv.first << "\t" << kv.second << "\n" ;
        }
        for ( const auto& kv : map2 ) {
            if ( both.count(kv.first) ) continue ;
            f3 << kv.first << "\t" << kv.second << "\n" ;
        }
    }
    f3.close() ;
    return 0;
}

【问题讨论】:

  • 您使用的是优化版本吗?您是否有足够的 RAM 在不使用分页文件的情况下将所有内容保存在内存中?
  • endl 替换为'\n'endl每次都刷新文件,效率很低。
  • 我已将 endl 替换为 '\n',但程序加速了一点,但还是谢谢你 - 我对流更熟悉了。
  • @1201ProgramAlarm、-O3-Ofast。我猜是。如何使用 PowerShell 检查:在操作系统开始使用分页文件之前有多少可用 RAM?

标签: c++ file io


【解决方案1】:

至少在这些文件中有大量不同键的限制(我假设您在这里已经足够接近),限制因素将是map 键查找,而不是 IO。在std::map 中查找键的时间复杂度为O(ln n),其中n 是容器中不同键的数量。

使用平均摊销了O(1) 键查找时间的std::unordered_map

在您开始根据错误假设进行优化之前,请使用分析器告诉您大部分时间都花在了哪里。在这种情况下也这样做以验证我是否正确。

【讨论】:

  • 感谢您的快速答复。你的想法很棒。事实上,我不需要排序键来查找键。但程序加速了一点。我已经用 Python 实现了相同的算法:它用了 195 秒,而用 C++ 用了 1152 秒。我不明白为什么?
  • @IvanVodopyanov 如果您更改为unordered_map,那么就平均渐近时间复杂度而言,真的没有什么需要改进的了。其余的归结为改善线性时间因子。 ThomasMatthews 给出了更全面的可能的此类瓶颈列表。使用分析器(例如 linux 上的 perf)来确定您的程序在哪些类别中花费的时间最多。您也可以在问题中添加 python 版本,以便我们可以验证程序是否确实这样做。
  • @IvanVodopyanov 您还可以添加一个 python 脚本(或其他)来生成输入文件,然后任何人都可以自己对其进行基准测试/分析(如果有人有时间这样做的话)
【解决方案2】:

瓶颈

您的瓶颈是 1) 文件 I/O 和 2) 格式转换。文件可能需要开销,例如旋转(硬盘驱动器)、寻道时间和实际读取时间。

流式传输数据

无论文件位于闪存驱动器还是硬盘驱动器上,流式传输数据都是最有效的(即保持数据流动)。例如,在 1 个事务中读取 1k 数据比在 1k 个事务中读取 1k 数据效率更高。您在这里最有效的方法是将大量数据读入内存。您可能需要检查您的操作系统以查看它是否支持内存映射。

转换数据

您的下一个瓶颈是解析数据并将其从文本格式转换为内部数字格式。这需要时间。为了加快速度,请将您的数据以二进制(内部)表示形式写入。由于字节序或浮点问题,这可能是不可能的。

存储数据

我想下一个瓶颈是将内部格式化数据存储到数据结构中。对于键、值、对,您可能需要使用std::mapstd::unordered_map。如果您不关心搜索时间,您可能希望使用这些值创建一个 struct 并将其存储到一个数组中(如果在编译时数量未知,则为 std::vector)。

分析

找出瓶颈所在的最佳方法是分析。运行您的应用程序 1E6 次重复并取平均值(您可能需要运行 1E9 次以获得更好的准确性)。在 Internet 上搜索“基准 C++ 程序”。基准测试将向您展示如何获得更好的结果。

【讨论】:

  • 谢谢你,托马斯。我不明白“存储数据”部分。当然,我确实关心搜索时间,所以你建议我不要使用 structvector?
  • 一切都取决于。在互联网上搜索“数据库理论索引表”。 structclass 擅长对数据进行建模。您可以将所有数据放入vector,但未排序的向量没有很好的搜索时间。在数据库领域,所有数据都放入一个表(也称为向量)中,然后构建索引表。索引表 (std::map) 包含搜索键和对vector 的索引。索引表提供了比每次都使用向量更快的搜索时间。
  • 如果你不知道编译时的数据量,另一个瓶颈将是数据库的扩展(vector或数组)。该向量将为 X 个项目分配空间。当您在 X 项之后 push_back() 时,向量需要展开。扩展涉及分配新的、更大的内存,将旧内存复制到新内存;都占用时间。您可以通过让您的数据源在数据文件的标题部分指定记录数(如果可能)来减少这种情况。
  • 我经常通过将文件块读取到缓冲区来加速程序。这些块大约为 1MB。另一层效率是使用多个缓冲区和线程。读取器线程读取缓冲区,然后向计算线程发出信号。然后阅读器从下一个缓冲区开始。这允许阅读器连续操作,计算线程执行该操作。缓冲区的数量和大小需要根据您的情况进行调整以达到最佳效率(一种大小并不适合所有人)。在网上搜索“双缓冲多个缓冲区”
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-08
  • 2013-02-03
  • 1970-01-01
  • 2015-03-27
  • 2010-12-15
相关资源
最近更新 更多