为什么 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的分配(和具有巨大堆栈分配缓冲区(仍为59MB(的pubsetbuf。当在gcc10.2.0和clang11.0.1上编译时,当使用来自gcc-libs10.2.0的/usr/lib/libstdc++.so.6和来自glibc2.32的/usr/lib/libc.so.6时,行为再现。系统区域设置为en_US.UTF-8,但如果我设置环境变量LC_ALL=C,这也会重现。

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

根据评论中的建议,我尝试了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环境中编译时分配大量堆内存?这是他们<fstream>中的一个错误吗?

正如在评论讨论中发现的那样,该程序的实际内存占用是完全合理的(84kb(,它只是分配和释放了数十万次相同的小内存,这在使用ASAN等避免重复使用堆空间的自定义分配器时会产生问题。我在";ASAN";数量

Stack Overflow用户@KamilCuk慷慨捐助了一个在其CI管道中重现该问题的gitlab项目。

事实并非如此。valgrind显示的数字59302487是所有分配的,并不代表程序的实际内存消耗。

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

我没有检查libc++源代码,但很有可能它在堆栈上使用了暂存空间,而不是堆。

正如评论中所确定的那样,您真正的问题是在AddressSanitizer下运行它,它延迟了释放内存的重用,以帮助在出现错误后获得使用。我对如何解决这一问题有一些想法(并非双关语(,并将把它们发布在"我如何从ASAN中严格排除分配?"?

不幸的是,基于C++流的I/O库通常没有得到充分利用,因为每个人都"知道";它的性能很差,所以存在一个先有鸡后有蛋的问题——坏的意见导致很少使用,导致漏洞报告稀疏,导致修复压力很低。

我想说C++流的最大用户是基础CS/IT教育部门;快速一次性脚本";(这肯定会比作者活得更长(,而且没有人真正关心表现。

你看到的只是一个浪费的实现——它不断地在内部的某个地方进行分配和释放,但据我所知,它并没有泄漏内存。我不认为有任何类型的";图案";这将保证在使用流I/O时以非脆性的方式获得更好的性能。

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

相关内容

最新更新