如何在 Rust 中调试内存问题?

Posted

技术标签:

【中文标题】如何在 Rust 中调试内存问题?【英文标题】:How do I debug a memory issue in Rust? 【发布时间】:2016-11-10 07:56:27 【问题描述】:

我希望这个问题不要太开放。我遇到了 Rust 的内存问题,我得到了an "out of memory" from calling next on an Iterator trait object。我不确定如何调试它。打印只是把我带到了失败发生的地步。我对ltrace 等其他工具不是很熟悉,所以虽然我可以创建跟踪(231MiB,pff),但我真的不知道如何处理它。这样的痕迹有用吗?我会更好地抓住 gdb/lldb 吗?还是 Valgrind?

【问题讨论】:

Valgrind 总是一个好的开始。尝试优化您的代码。 @EliSadoff 优化我的代码对调试内存问题有何帮助? 【参考方案1】:

Valgrind 和其他工具工作正常,从 Rust 1.32 开始应该可以开箱即用。早期版本的 Rust 需要将全局分配器从 jemalloc 更改为系统的分配器,以便 Valgrind 和朋友知道如何监控内存分配。

在这个答案中,我使用 macOS 开发工具 Instruments,就像我在 macOS 上一样,但 Valgrind / Massif / Cachegrind 的工作方式类似。

示例:无限循环

这是一个通过将 1MiB Strings 推入 Vec 并且从不释放它来“泄漏”内存的程序:

use std::thread, time::Duration;

fn main() 
    let mut held_forever = Vec::new();
    loop 
        held_forever.push("x".repeat(1024 * 1024));
        println!("Allocated another");

        thread::sleep(Duration::from_secs(3));
    

您可以看到内存随时间的增长,以及分配内存的确切堆栈跟踪:

示例:引用计数中的循环

这是一个通过创建无限引用循环来泄漏内存的示例:

use std::cell::RefCell, rc::Rc;

struct Leaked 
    data: String,
    me: RefCell<Option<Rc<Leaked>>>,


fn main() 
    let data = "x".repeat(5 * 1024 * 1024);

    let leaked = Rc::new(Leaked 
        data,
        me: RefCell::new(None),
    );

    let me = leaked.clone();
    *leaked.me.borrow_mut() = Some(me);

另见:

Why does Valgrind not detect a memory leak in a Rust program using nightly 1.29.0? Handling memory leak in cyclic graphs using RefCell and Rc Minimal `Rc` Dependency Cycle

【讨论】:

【参考方案2】:

一般我会尝试以下方法:

    样板缩减:尽量缩小 OOM 的问题范围,这样您就不会有太多额外的代码。换句话说:你的程序崩溃越快越好。有时也可以将一段特定的代码撕下来,放入一个额外的二进制文件中,仅用于调查。

    问题规模缩小:将问题从 OOM 降低为简单的“内存过多”,这样您就可以真正看出某些部分浪费了一些东西,但不会导致 OOM。如果很难判断您是否看到问题,您可以降低内存限制。在 Linux 上,可以使用ulimit

    ulimit -Sv 500000  # that's 500MB
    ./path/to/exe --foo
    

    信息收集:如果您的问题足够小,您就可以收集噪音级别较低的信息。您可以尝试多种方法。只要记住用调试符号编译你的程序。关闭优化也可能是一个优势,因为这通常会导致信息丢失。两者都可以通过在编译期间不使用--release 标志来归档。

    堆分析: 一种方法是使用gperftools:

    LD_PRELOAD="/usr/lib/libtcmalloc.so" HEAPPROFILE=/tmp/profile ./path/to/exe --foo
    pprof --gv ./path/to/exe /tmp/profile/profile.0100.heap
    

    这向您显示了一个图表,该图表表示您的程序的哪些部分占用了多少内存。详情请见official docs。

    rr: 有时很难弄清楚实际发生了什么,尤其是在您创建了个人资料之后。假设你在第 2 步中做得很好,你可以使用rr:

    rr record ./path/to/exe --foo
    rr replay
    

    这将产生一个具有超能力的 GDB。与正常调试会话的不同之处在于,您不仅可以continue,还可以reverse-continue。基本上,您的程序是从录音中执行的,您可以根据需要来回跳转。 This wiki page 为您提供了一些额外的示例。需要指出的一件事是 rr 似乎只适用于 GDB。

    很好的旧调试:有时你会得到仍然太大的痕迹和记录。在这种情况下,您可以(结合ulimit 技巧)只使用 GDB 并等到程序崩溃:

    gdb --args ./path/to/exe --foo
    

    您现在应该获得一个正常的调试会话,您可以在其中检查程序的当前状态。 GDB 也可以使用 coredump 启动。这种方法的一般问题是您无法及时返回,也无法继续执行。因此,您只能看到当前状态,包括所有堆栈帧和变量。如果你愿意,你也可以在这里使用 LLDB。

    (潜在)修复 + 重复: 粘贴好可能出错的地方后,您可以尝试更改代码。然后再试一次。如果仍然无法正常工作,请返回第 3 步并重试。

【讨论】:

【参考方案3】:

一般来说,要进行调试,您可以使用基于日志的方法(通过自己插入日志,或使用 ltraceptrace 等工具为您生成日志)或者你可以使用调试器。

请注意,ltraceptrace 或基于调试器的方法要求您能够重现问题;我倾向于使用手动日志,因为我在一个错误报告通常过于不精确而无法立即复制的行业中工作(因此我们使用日志来创建复制器场景)。

Rust 支持这两种方法,用于 C 或 C++ 程序的标准工具集非常适合它。

我个人的方法是设置一些日志记录以快速缩小问题发生的范围,如果日志记录不足以启动调试器进行更精细的检查。在这种情况下,我建议直接使用调试器。

生成了一个panic,这意味着通过中断对恐慌挂钩的调用,您可以看到出现问题时的调用堆栈和内存状态。

用调试器启动你的程序,在恐慌钩子上设置一个断点,运行程序,获利。

【讨论】:

以上是关于如何在 Rust 中调试内存问题?的主要内容,如果未能解决你的问题,请参考以下文章

如何在生产环境排查 Rust 内存占用过高问题

如何在生产环境排查 Rust 内存占用过高问题

Rust 中的精确内存布局控制?

如何将 16 字节的内存加载到 Rust __m128i 中?

为啥 Rust 认为泄漏内存是安全的?

如何声明静态变量作为硬编码内存地址的引用?