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

Reading large (~1GB) data file with C++ sometimes throws bad_alloc, even if I have more than 10GB of RAM available

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

为此,我使用了 this SO answer 中的 slurp 函数。 问题是代码有时但并非总是会抛出 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;
}

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

堆(用于动态分配的内存池)的大小受限于您​​机器的 RAM 数量。对于如此大的分配,您应该使用其他一些内存分配技术,这可能会迫使您更改从文件中读取的方式。

如果您 运行 在基于 UNIX 的系统上,如果您 运行 在 Windows 平台上,您可以检查函数 vmalloc 或 VirtualAlloc 函数。

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

我还认为 std::string 永远不会 如果实际处理这么大的文件(可能是二进制文件),则不会选择数据类型。

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

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

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

如果你以前不知道Boost,现在有点像"the extended standard library for C++",当然,它有一个class抽象内存映射文件:mapped_file.

The file I'm reading contains a series of data in ASCII tabular form, i.e. float1,float2\nfloat3,float4\n....

I'm browsing through the various possible solutions proposed on SO to deal with this kind of problem, but I was left wondering on this (to me) peculiar behaviour. What would you recommend in these kinds of circumstances?

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

I'm still not getting something: you say that file IO is much slower than in-memory parsing, and this I understand (and is the reason why I wanted to read the whole file at once). Then you say that the best way is to parse the whole file incrementally into an in-memory array of float. What exactly do you mean by this? Doesn't this mean to read the file line-by-line, resulting in a large number of file IO operations?

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

现在的想法是,您偶尔会切换上下文,而且事实上,如果您保持单线程,有时您不会读取文件,因为您仍在忙于将文本转换为float 仍然意味着较少的性能影响,因为大多数时候,您的 read 几乎会立即 return,因为您的 OS/runtime 已经预取了文件的重要部分。

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

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

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

-1.23456E-10

平均 11 个字符。

每个数字后加一个,\n

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

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

在 32 位可执行文件中,总内存地址 space 为 4gb。其中,有时会保留 1-2 GB 供系统使用。

要分配 1 GB,您需要 1 GB 的连续 space。要复制它,您需要 2 个 1 GB 的块。这很容易出乎意料地失败。

有两种方法。首先,切换到 64 位可执行文件。这不会 运行 在 32 位系统上。

其次,停止分配 1 GB 的连续块。一旦你开始处理那么多数据,对其进行分段和/或流式传输就变得很有意义了。如果做得好,您还可以在阅读完之前开始处理它。

有很多文件 io 数据结构,从 stxxl 到 boost,或者您可以自己动手。