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

时间:2016-07-07 20:43:41

标签: memory out-of-memory rust

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

3 个答案:

答案 0 :(得分:5)

一般情况下,我会尝试采用以下方法:

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

  2. 问题规模减少:将问题从OOM降低到简单的“太多内存”,以便您可以实际告诉某些部分浪费了一些东西,但它不会导致OOM 。如果您很难分辨出问题,可以降低内存限制。在Linux上,可以使用ulimit

    完成此操作
    ulimit -Sv 500000  # that's 500MB
    ./path/to/exe --foo
    
  3. 信息收集:如果您的问题足够小,您就可以收集噪音较低的信息了。您可以尝试多种方式。请记住使用调试符号编译程序。关闭优化也可能是有利的,因为这通常会导致信息丢失。两者都可以在编译期间使用--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也可以使用coredumps启动。这种方法的一般问题是你不能回到过去,你不能继续执行。所以你只能看到包括所有堆栈帧和变量的当前状态。如果您愿意,也可以在这里使用LLDB。

  4. (潜在)修复+重复:粘贴后可能会出现问题,您可以尝试更改代码。然后再试一次。如果它仍然无效,请返回步骤3再试一次。

答案 1 :(得分:4)

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

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

示例:无限循环

这是一个通过将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 <code>Rc</code> leak

另请参阅:

答案 2 :(得分:3)

通常,要进行调试,您可以使用基于日志的方法(通过自己插入日志,或使用422 Unprocessable Entity ltrace等工具来生成为您记录日志)或您可以使用调试器。

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

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

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

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

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