为什么GCC的ifstream>>双分配这么多内存?

我需要从一个以空格分隔的人类可读文件中读取一系列数字并进行一些数学运算,但是我在读取文件时遇到了一些真正奇怪的内存行为。

如果我阅读数字并立即丢弃它们......

#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 >> 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()->pubsetbuf(0, 0);(这使得程序需要很长时间并且仍然进行 59MB 的分配)和pubsetbuf巨大的堆栈分配缓冲区(仍然是 59MB)。行为再现当对编译gcc10.2.0和clang11.0.1使用时/usr/lib/libstdc++.so.6gcc-libs10.2.0和/usr/lib/libc.so.6glibc2.32。系统区域设置为,en_US.UTF-8但如果我设置环境变量,这也会重现LC_ALL=C

我第一次注意到这个问题的 ARM CI 环境是在 Ubuntu Focal 上使用GCC 9.3.0、libstdc++610.2.0和2.31交叉编译的libc

按照评论中的建议,我尝试了 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. 在构建或使用ifstreamGNU 环境中编译时,我是否可以做一些不同的事情来避免分配大量的堆内存?这是他们的错误<fstream>吗?

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

一个再现其CI管道问题gitlab项目慷慨贡献的堆栈溢出用户@KamilCuk。

回答

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

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

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

正如评论中所确定的,您真正的问题是您在 AddressSanitizer 下运行它,这会延迟释放内存的重用,以帮助捕获释放后使用错误。我对如何解决这个问题有一些想法(没有双关语),并将它们发布在如何从 ASAN 的紧密循环中排除分配?


以上是为什么GCC的ifstream&gt;&gt;双分配这么多内存?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>