在一个之前的问题中,发现使用最新版本的GNU libstdc++从以空格分隔的可读文件(镜像)中读取一系列数字会导致大量分配内存,与文件大小成线性比例增长。
给定上面链接的文件和此测试程序:
使用这个预处理器门可以使总内存占用更加可控:
这是因为libstdc++的
给定上面链接的文件和此测试程序:
#include <fstream>
int main(int, char**) {
std::ifstream ww15mgh("ww15mgh.grd");
double value;
while (ww15mgh >> value);
return 0;
}
Valgrind --tool=memcheck
报告如下:
==523661== total heap usage: 1,038,970 allocs, 1,038,970 frees, 59,302,487
因为那一百万个分配都在operator>>
返回之前立即被释放,所以没有泄漏,程序在发布构建中的实际内存占用非常小(81KB)。但是,使用-fsanitize=address
编译会将这些大量的分配变成一个真正的问题。
以下是运行该程序时启用和禁用ASAN的总内存占用情况:
$ g++ stackoverflow.cpp -o _build/stackoverflow
$ /usr/bin/time -v _build/stackoverflow |& grep 'm r'
Maximum resident set size (kbytes): 3512
$ g++ stackoverflow.cpp -o _build/stackoverflow_asan -fsanitize=address
$ /usr/bin/time -v _build/stackoverflow_asan |& grep 'm r'
Maximum resident set size (kbytes): 125196
125MB听起来并不是一个很大的问题,但在一个更大的单元测试程序上下文中运行,并且在一个嵌入式ARM板上多次调用此函数时,它使我的ARM CI环境内存耗尽了。
对于这种特定情况的解决方法
#include <fstream>
#include <string>
#include <cstdio>
int main(int, char**) {
std::ifstream ww15mgh("ww15mgh.grd");
double value;
#if defined(__SANITIZE_ADDRESS__) && (defined(__GLIBCXX__) || defined(__GLIBCPP__))
std::string text;
while (ww15mgh >> text)
value = std::strtod(text.data(), nullptr);
#else
while (ww15mgh >> value);
#endif
return 0;
}
使用这个预处理器门可以使总内存占用更加可控:
$ g++ stackoverflow_workaround.cpp -o _build/stackoverflow_workaround_asan -fsanitize=address
$ /usr/bin/time -v _build/stackoverflow_workaround_asan |& grep 'm r'
Maximum resident set size (kbytes): 6396
这是因为libstdc++的
operator>>(ifstream&, string&)
和glibc的strtod
都没有多余的分配,可以通过在valgrind下运行绕过解决方法来看到这一点。$ g++ stackoverflow_workaround.cpp -D__SANITIZE_ADDRESS__
$ valgrind --tool=memcheck --leak-check=yes ./a.out |& grep 'total heap'
==2483624== total heap usage: 3 allocs, 3 frees, 81,368 bytes allocated
目前,我的CI不再因为内存耗尽而崩溃了,所以我的同事们可以继续他们的工作。但是,我感觉使用#ifdef __SANITIZE_ADDRESS__
隐藏某些东西似乎有点作弊。
问题
是否有一种方法可以让原始程序在ASAN下运行,但在operator>>
调用期间跳过ASAN的分配器填充?在紧密循环调用第三方函数分配内存的一般情况下,如何避免使用-fsanitize=address
产生巨大的内存占用?