如何在一个紧密循环中排除 ASAN 的分配?

5
在一个之前的问题中,发现使用最新版本的GNU 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流水线结果可在GitLab上获取。

目前,我的CI不再因为内存耗尽而崩溃了,所以我的同事们可以继续他们的工作。但是,我感觉使用#ifdef __SANITIZE_ADDRESS__隐藏某些东西似乎有点作弊。

问题

是否有一种方法可以让原始程序在ASAN下运行,但在operator>>调用期间跳过ASAN的分配器填充?在紧密循环调用第三方函数分配内存的一般情况下,如何避免使用-fsanitize=address产生巨大的内存占用?


等等,等等,你在同时使用ValgrindAddress Sanitizer吗?为什么?当你有地址检查器时,你不需要Valgrind。在编译器提供工具来检测错误之前,Valgrind是很有用的。 - Marek R
1
不,我并没有同时使用它们。问题中的valgrind堆分配报告都是在没有启用“-fsanitize = address”的二进制文件上进行的,并且仅被包括在其中以解释为什么在gnu libstdc ++下执行测试程序时地址污染器会耗尽内存。 - Dan
1个回答

4
正如你所说,AddressSanitizer将延迟释放的内存重用,以帮助捕获使用后释放的错误。这个功能被称为“隔离”,其使用的内存量可以在运行时进行配置,请参见https://github.com/google/sanitizers/wiki/AddressSanitizerFlags。例如,如果在运行程序之前将环境变量ASAN_OPTIONS设置为quarantine_size_mb=4,它应该将使用的内存量限制为4兆字节。
这与所讨论的调用无关,因此它并没有完全解决你所问的问题,但我认为它会解决你的根本问题:“如何在内存较低的计算机上使用AddressSanitizer”。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接