问题描述
我需要从 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.6
和 clang
11.0.1 中的 /usr/lib/libc.so.6
在 gcc-libs
10.2.0 和 glibc
2.32 上编译时,行为会重现。系统区域设置为 en_US.UTF-8
,但如果我设置环境变量 LC_ALL=C
,这也会重现。
我第一次发现问题的 ARM CI 环境是在 Ubuntu Focal 上使用 GCC 9.3.0、libstdc++6
10.2.0 和 libc
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
。