使用 C++ 读取大型(~1GB)数据文件有时会抛出 bad_alloc,即使我有超过 10GB 的可用 RAM

glS*_*glS 2 c++ file-io bad-alloc

我正在尝试读取大小为 ~1.1GB 的 .dat 文件中包含的数据。因为我是在 16GB RAM 的机器上执行此操作,所以我认为将整个文件一次读入内存不会有问题,只有在处理它之后。

为此,我使用了slurp这个 SO answer 中找到的函数。问题是代码有时(但并非总是)抛出 bad_alloc 异常。查看任务管理器,我发现总有至少 10GB 的可用内存可用,所以我不知道内存会是什么问题。

这是重现此错误的代码

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>

using namespace std;

int main()
{
    ifstream file;
    file.open("big_file.dat");
    if(!file.is_open())
        cerr << "The file was not found\n";

    stringstream sstr;
    sstr << file.rdbuf();
    string text = sstr.str();

    cout << "Successfully read file!\n";
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

什么可能导致这个问题?避免它的最佳做法是什么?

Mar*_*ler 5

您的系统有 16GB 的事实并不意味着任何程序在任何时候都可以分配给定数量的内存。事实上,这可能适用于只有 512MB 物理 RAM 的机器,如果有足够的交换可用,或者它可能会在具有 128GB RAM 的 HPC 节点上失败——这完全取决于您的操作系统来决定有多少可用内存给你,在这里。

我还认为,std::string从来没有,如果实际处理文件,可能是二进制,即大量可供选择的数据类型。

这里的重点是绝对不知道stringstream尝试分配多少内存。每当分配的内部缓冲区变得太小而无法包含传入的字节时,一个非常合理的算法会将分配的内存量加倍。此外,libc++/libc 可能也有自己的分配器,它们会有一些分配开销,在这里。

请注意,stringstream::str()返回包含在的内部状态中的数据的副本stringstream,再次为您留下至少 2.2 GB 的堆用于此任务。

确实,如果您需要将大型二进制文件中的数据作为可以使用索引运算符访问的内容来处理[],请查看内存映射您的文件;这样,您将获得一个指向文件开头的指针,并且可以将其用作内存中的普通数组,让您的操作系统负责处理底层内存/缓冲区管理。这就是操作系统的用途!

如果你以前不知道Boost,它现在有点像“C++ 的扩展标准库”,当然,它有一个抽象内存映射文件的类:mapped_file.

我正在阅读的文件包含一系列 ASCII 表格形式的数据,即 float1,float2\nfloat3,float4\n....

我正在浏览 SO 上提出的各种可能的解决方案来处理此类问题,但我对这种(对我而言)奇怪的行为感到疑惑。在这种情况下,您会推荐什么?

要看; 我实际上认为处理这个问题的最快方法(因为文件 IO 比内存中的 ASCII 解析要慢得多)是增量地解析文件,直接将其解析为内存中的float变量数组;可能会利用您的操作系统的预取 SMP 功能,因为如果您为文件读取和浮点转换生成单独的线程,您甚至不会获得太多的速度优势。std::copy,用于读取std::ifstream到 astd::vector<float>应该可以正常工作,在这里。

我仍然没有得到一些东西:你说文件 IO 比内存中解析慢得多,我明白这一点(这也是我想一次读取整个文件的原因)。然后你说最好的方法是将整个文件增量解析为内存中的浮点数组。这到底是什么意思?这不是说要逐行读取文件,导致大量的文件IO操作吗?

是的,也不是:首先,当然,您将有更多的上下文切换,如果您只是订购一次阅读整个内容,那么您将拥有更多的上下文切换。但那些并不那么昂贵——至少,当你意识到大多数操作系统和 libc 非常了解如何优化读取时,它们会便宜得多,因此如果你不要使用极其随机的read长度。此外,您不会推断尝试分配大小至少为 1.1GB 的 RAM 块的惩罚——这需要一些严重的页表查找,但速度也不是那么快。

现在,这个想法是您偶尔的上下文切换以及事实是,如果您保持单线程,有时您会因为仍在忙于将文本转换为浮动而没有读取文件,这仍然意味着更少性能受到影响,因为大多数情况下,您read几乎会立即返回,因为您的操作系统/运行时已经预取了文件的重要部分。

一般来说,对我来说,你似乎担心所有错误的事情:性能对你来说似乎很重要(这里真的那么重要吗?你正在使用一种脑死文件格式来交换浮点数,这是两者都臃肿,丢失信息,而且解析速度很慢),但您宁愿先一次读取整个文件,然后开始将其转换为数字。坦率地说,如果性能对您的应用程序至关重要,您将开始多线程/处理它,这样字符串解析就可能在数据仍在读取时发生。使用几千到兆字节的缓冲区进行读取\n 边界并与创建内存中浮点表的线程交换听起来好像它基本上可以将您的读取+解析时间减少到读取+不可测量而不会牺牲读取性能,并且不需要 GB 的 RAM 来解析一个顺序文件。

顺便说一下,为了让您了解在 ASCII 中存储浮点数有多糟糕:

典型的 32 位单精度 IEEE753 浮点数大约有 6-9 位有效十进制数字。因此,您将需要至少 6 个字符来用 ASCII 表示这些字符,一个.,通常是一个指数除法器,例如E,平均 2.5 位十进制指数,加上平均半个符号字符(-或不是),如果您的数字是统一的从所有可能的 IEEE754 32 位浮点数中选择:

-1.23456E-10
Run Code Online (Sandbox Code Playgroud)

这是平均 11 个字符。

添加一个,\n在每个数字之后。

现在,您的字符是 1B,这意味着您将 4B 的实际数据放大了 3 倍,但仍然失去了精度。

现在,人们总是过来告诉我纯文本更有用,因为如果有疑问,用户可以阅读它......我还没有看到一个用户可以浏览 1.1GB(根据我上面的计算,大约 9000 万个)浮点数,或 4500 万个浮点对)而不会发疯。