首页 文章

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

提问于
浏览
5

我希望这个问题不是太开放 . 我遇到了Rust的内存问题,我得到了an "out of memory" from calling next on an Iterator trait object . 我对ltrace之类的其他工具不太熟悉,所以虽然我可以创建一个跟踪(231MiB,pff),但我真的不知道如何处理它 . 这样的痕迹有用吗? grab gdb / lldb会更好吗?还是Valgrind?

3 回答

  • 3

    一般来说,我会尝试采用以下方法:

    • Boilerplate reduction: 尝试缩小OOM的问题范围,以便您没有太多额外的代码 . 换句话说:程序崩溃越快越好 . 有时也可以删除特定的代码片段并将其放入额外的二进制代码中,仅用于调查 .

    • Problem size reduction: 将问题从OOM降低到简单的"too much memory",这样你实际上可以告诉某些部分浪费了一些东西,但它不会导致OOM . 如果您很难分辨出问题,可以降低内存限制 . 在Linux上,这可以使用 ulimit 来完成:

    ulimit -Sv 500000  # that's 500MB
    ./path/to/exe --foo
    
    • Information gathering: 如果您的问题足够小,您就可以收集噪音水平较低的信息了 . 您可以尝试多种方式 . 请记住使用调试符号编译程序 . 关闭优化也可能是有利的,因为这通常会导致信息丢失 . 两者都可以在编译期间使用 --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:有时很难弄清楚实际发生了什么,尤其是在您创建了 Profiles 之后 . 假设你在第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也可以使用coredumps启动 . 这种方法的一般问题是你不能回到过去,你不能继续执行 . 所以你只能看到包括所有堆栈帧和变量的当前状态 . 如果需要,您也可以在这里使用LLDB .

    • (Potential) fix + repeat: 粘贴后可能会出现问题,您可以尝试更改代码 . 然后再试一次 . 如果仍然无法正常工作,请返回步骤3再试一次 .
  • 4

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

    在这个答案中,我使用macOS开发工具Instruments,因为我在macOS上,但Valgrind / Massif / Cachegrind的工作方式类似 .

    示例:无限循环

    这是一个程序"leaks"内存通过将1MiB String 推入 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));
        }
    }
    

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

    Instruments memory debugging

    示例:引用计数的周期

    以下是通过创建无限参考周期来泄漏内存的示例:

    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);
    }
    

    Instruments for Rc leak

    也可以看看:

  • 5

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

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

    Rust支持这两种方法,以及用于C或C的标准工具集程序效果很好 .

    我个人的方法是进行一些日志记录,以便快速缩小问题发生的位置,并且如果日志记录不足以启动调试器以进行更精细的检查 . 在这种情况下,我建议立即去调试器 .

    生成 panic ,这意味着通过断开对恐慌钩子的调用,您可以在出现问题时看到调用堆栈和内存状态 .

    使用调试器启动程序,在恐慌钩子上设置断点,运行程序,获利 .

相关问题