【问题标题】:Why does GCC's ifstream >> double allocate so much memory?为什么 GCC 的 ifstream >> double 会分配这么多内存?
【发布时间】:2021-04-18 12:59:18
【问题描述】:

我需要从a space-separated human-readable file 读取一系列数字并进行一些数学运算,但我在读取文件时遇到了一些真正奇怪的内存行为。

如果我读到这些数字并立即丢弃它们......

#include <fstream>

int main(int, char**) {
    std::ifstream ww15mgh("ww15mgh.grd");
    double value;
    while (ww15mgh >> value);
    return 0;
}

我的程序根据 valgrind 分配 59MB 内存,相对于文件大小线性缩放:

$ g++ stackoverflow.cpp
$ valgrind --tool=memcheck --leak-check=yes ./a.out 2>&1 | grep total
==523661==   total heap usage: 1,038,970 allocs, 1,038,970 frees, 59,302,487 

但是,如果我使用ifstream &gt;&gt; string 代替然后使用sscanf 来解析字符串,我的内存使用情况看起来更合理:

#include <fstream>
#include <string>
#include <cstdio>

int main(int, char**) {
    std::ifstream ww15mgh("ww15mgh.grd");
    double value;
    std::string text;
    while (ww15mgh >> text)
        std::sscanf(text.c_str(), "%lf", &value);
    return 0;
}
$ g++ stackoverflow2.cpp
$ valgrind --tool=memcheck --leak-check=yes ./a.out 2>&1 | grep total
==534531==   total heap usage: 3 allocs, 3 frees, 81,368 bytes allocated

为了排除 IO 缓冲区的问题,我尝试了 ww15mgh.rdbuf()-&gt;pubsetbuf(0, 0);(这使得程序需要很长时间并且仍然进行 59MB 的分配)和 pubsetbuf 使用巨大的堆栈分配缓冲区(仍然是 59MB )。当使用来自gcc-libs 10.2.0/usr/lib/libstdc++.so.6 和来自glibc 2.32/usr/lib/libc.so.6 时,在gcc 10.2.0clang 11.0.1 上编译时,该行为会重现。系统语言环境设置为en_US.UTF-8,但如果我设置环境变量LC_ALL=C,这也会重现。

我第一次注意到问题的 ARM CI 环境是在 Ubuntu Focal 上使用 GCC 9.3.0libstdc++6 10.2.0libc 2.31 进行交叉编译的。

advice in the comments 之后,我尝试了 LLVM 的 libc++,并在原始程序中获得了完全理智的行为:

$ clang++ -std=c++14 -stdlib=libc++ -I/usr/include/c++/v1 stackoverflow.cpp
$ valgrind --tool=memcheck --leak-check=yes ./a.out 2>&1 | grep total
==700627==   total heap usage: 3 allocs, 3 frees, 8,664 bytes allocated

因此,这种行为似乎是 GCC 的 fstream 实现所独有的。在构建或使用ifstream 时我可以做些什么不同的事情来避免在GNU 环境中编译时分配大量堆内存?这是他们&lt;fstream&gt; 中的错误吗?

正如在 cmets 讨论中发现的那样,程序的实际内存占用是完全正常的 (84kb),它只是分配和释放相同的一小部分内存数十万次,这在使用像 ASAN 这样的自定义分配器时会产生问题避免重复使用堆空间。我发布了a follow-up question,询问如何在“ASAN”级别处理此类问题。

gitlab project that reproduces the issue in its CI pipeline 由 Stack Overflow 用户 @KamilCuk 慷慨捐赠。

【问题讨论】:

  • 我不知道,但是 - 只是出于好奇 - 在检查内存消耗是否取决于数据大小时,我会准备两到十倍长的文件......
  • 看起来像istream 类的operator &gt;&gt;(double&amp;) 实现中的内存泄漏......仍然不知道如何修复它。 :(
  • 我不认为您的代码有什么问题,您可以尝试将 Clang 与 libc++ 一起使用,看看该实现是否使用更少的内存。
  • 在嵌入式 ARM 板上,它开始成为一个问题。在 ASAN(为每个分配中添加填充)下进行测试时,它实际上破坏了我的 ARM CI 运行器。而且,它是输入文件大小的 6.5 倍,这太荒谬了。
  • 对,程序的实际内存使用量是完全合理的 81 KB。所以我认为你真正的问题是如何配置 ASAN 以更有效地处理这种分配模式。

标签: c++ memory fstream libstdc++


【解决方案1】:

真的没有。 valgrind显示的数字59,302,487是所有分配的sum,并不代表程序的实际内存消耗。

事实证明,相关operator&gt;&gt; 的libstdc++ 实现为暂存空间创建了一个临时std::string,并为其保留了32 个字节。然后在使用后立即解除分配。见num_get::do_get。有了开销,这实际上可能分配了 56 个字节左右,乘以大约 100 万次重复确实意味着,在某种意义上,总共分配了 59 兆字节,当然这就是为什么这个数字与输入的数量成线性关系.但同样的 56 个字节被一遍又一遍地分配和释放。这是 libstdc++ 完全无辜的行为,不是泄漏或过多的内存消耗。

我没有检查 libc++ 源代码,但一个不错的选择是它使用堆栈上的暂存空间而不是堆。

在 cmets 中确定,您真正的问题是您在 AddressSanitizer 下运行它,这会延迟释放内存的重用,以帮助捕获 use-after-free 错误。我对如何解决这个问题有一些想法(不是双关语),并将它们发布在How do I exclude allocations in a tight loop from ASAN?

【讨论】:

    【解决方案2】:

    不幸的是,基于 C++ 流的 I/O 库通常没有得到充分利用,因为每个人都“知道”它的性能很差,所以那里存在先有鸡还是先有蛋的问题 - 不好的意见导致很少使用导致稀疏的错误报告导致低修复压力。

    我想说,C++ 流的最大用户是基础 CS/IT 教育部门和“快速一次性脚本”(总是比作者长寿),没有人真正关心性能。

    您所看到的只是一种浪费的实现——它不断地在内部的某个地方分配和取消分配,但据我所知,它并没有泄漏内存。我不认为有任何一种“模式”可以保证在使用流 I/O 时以非脆弱的方式获得更好的性能。

    在嵌入式环境中获胜的最佳策略是根本不玩游戏。忘记 C++ 流 I/O,一切都会好起来的。有其他格式化 I/O 库可以恢复 C++ 的类型安全性并表现得更好,然后您就不会受制于标准库实现错误/效率低下。或者,如果您不想添加依赖项,请使用 sscanf

    【讨论】:

    • 恕我直言,“在胆量中不断分配和取消分配”通常不是“浪费的实现”。这使得堆分配很慢的假设,尤其是这个。事实上,这种模式可以非常快。
    猜你喜欢
    • 2012-11-08
    • 2017-05-03
    • 2020-10-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-04
    • 2019-09-30
    • 2018-10-10
    相关资源
    最近更新 更多