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

问题描述

我需要从 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,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)。当使用 gcc 10.2.0 中的 /usr/lib/libstdc++.so.6clang 11.0.1 中的 /usr/lib/libc.so.6gcc-libs 10.2.0glibc 2.32 上编译时,行为会重现。系统区域设置为 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,8,664 bytes allocated

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

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

一个 gitlab project that reproduces the issue in its CI pipeline 由 Stack Overflow 用户 @KamilCuk 慷慨提供。

解决方法

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

我想说 C++ 流的最大用户是基本的 CS/IT 教育领域和“快速一次性脚本”(这总是比作者活得更久),没有人真正关心性能。

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

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

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...