#cycle #zero #risc #risc0 #host #profiler #side

l2r0-profiler-host

RISC Zero的性能分析器,主机端程序

1 个不稳定版本

0.21.0 2024年2月12日
0.20.1 2024年2月12日

#215 in 性能分析

MIT/Apache

30KB
430

使用此RISC Zero性能分析器如履平地

A young boy walking on water heading to a place with Bonsai.

本仓库提供了一个RISC Zero程序的插件,它可以计算程序不同部分贡献的周期数,检测导致大量周期数的执行步骤,并解释其根本原因。

开发者可以在程序中添加 start_timer!stop_start_timer!stop_timer! 来跟踪周期的来源。以下是一个示例。

start_timer!("Load data");
......

    start_timer!("Read from the host");
    ......

    stop_start_timer!("Check the length");
    ......

    stop_start_timer!("Hash");
    ......

    stop_timer!();

stop_timer!();

分析器将输出有关周期分解的彩色信息。具体来说,如果分析器看到一个执行步骤,尽管如此,但导致了大量周期,它将突出显示并找出根本原因。

有人可能会问,我们为什么说这个分析器是“如履平地”。这是因为,与基于 eprintln! 的先前解决方案不同,分析器本身非常努力地不干扰程序的原始执行,特别是周期计数。

An example output of the profiler.

如何使用?

在RISC Zero程序的主机和虚拟机都需要进行必要的更改。

主机

主机应使用主机crate l2r0-profiler-host 并使用 ExecutorEnv 来运行程序。

let cycle_tracer = Rc::new(RefCell::new(CycleTracer::default()));

let env = ExecutorEnv::builder()
        .write_slice(&task.a)
        .write_slice(&task.b)
        .write_slice(&task.long_form_c)
        .write_slice(&task.k)
        .write_slice(&task.long_form_kn)
        .trace_callback(|e| {
            cycle_tracer.borrow_mut().handle_event(e);
            Ok(())
        })
        .build()
        .unwrap();

let mut exec = ExecutorImpl::from_elf(env, METHOD_ELF).unwrap();
let _ = exec.run().unwrap();

cycle_tracer.borrow().print();

在上面的示例中,我们首先创建周期跟踪器。

let cycle_tracer = Rc::new(RefCell::new(CycleTracer::default()));

然后,我们使用 trace_callback 请求 ExecutorEnv 将执行跟踪发送回分析器。

.trace_callback(|e| {
    cycle_tracer.borrow_mut().handle_event(e);
    Ok(())
})

执行完成后,请求周期跟踪器输出分析结果。

cycle_tracer.borrow().print();

虚拟机

虚拟机也有自己的crate,l2r0-profiler-guest。导入它,并记得开启 print-trace 功能。

当程序启动时,如下所示。

fn main() {
    l2r0_profiler_guest::init_trace_logger();
    start_timer!("Total");
    ......
    stop_timer!();
}

我们首先初始化跟踪记录器。

l2r0_profiler_guest::init_trace_logger();

然后,虚拟机可以使用宏将程序分解成更小的部分进行检查。

它如何工作?

分析器的工作方式类似于硬件观察点。

当客户程序启动时,客户端周期跟踪器使用一些虚拟指令(写入零寄存器)来通知主机端周期跟踪器监视的缓冲区。代码在这里:这里

#[inline(always)]
pub fn init_trace_logger() {
    unsafe {
        core::arch::asm!(
            r#"
            nop
            li x0, 0xCDCDCDCD
            la x0, TRACE_MSG_CHANNEL
            la x0, TRACE_MSG_LEN_CHANNEL
            la x0, TRACE_SIGNAL_CHANNEL
            nop
        "#
        );
    }
}

然后,主机端周期跟踪器将监视这三个通道。如果程序将这些数据写入这些内存位置,主机端周期跟踪器可以捕捉这些变化并获取通道中的信息。

例如,

  • start_timer!(msg) 将消息 msg 复制到 TRACE_MSG_CHANNEL,并将消息长度写入 TRACE_MSG_LEN_CHANNEL,这会触发主机端周期跟踪器标记新的计时器已开始。
  • end_timer!() 将零写入 TRACE_SIGNAL_CHANNEL,这会触发主机端周期跟踪器标记之前的计时器已停止。

这两个计时器都设计得非常简洁,因为我们希望它们不会占用太多的周期。这与之前在客户端使用 eprintln!("{}", env::get_cycle_count()); 的方法相比,这本身就会创建很多周期并影响计算。

限制

请注意,分析器只能看到内存写入,但不能看到内存读取。因此,尽管周期计数是正确的,但分析器只能解释一小部分重要的指令,即周期的来源。更具体地说,

  • 如果一条指令将干净的页变为脏页,分析器可以解释哪个页被变脏。
  • 如果一条指令加载了一个新的页,分析器不能解释这个指令加载的是哪个页。
  • 如果一条指令加载了一个新的页并立即将其变脏(这是从堆中分配的情况),分析器可以解释与脏页相关的周期,但不能解释与首次加载的页相关的周期。

另一个限制是,当新的段开始时发生的所有周期都会计入其后立即执行的第一条指令。这些指令会有大量的周期,原因如下。

  • 如果指令原本计划放入上一个段,但空间不足,上一个段将提前关闭并使用虚拟周期。

  • 当新段开始时,会有一些预加载和后加载的周期。

  • 由于新段中没有加载的页面,也没有任何页面被标记为脏。这条指令可能会触发许多页面操作。假设这条指令是一个256位模幂减的系统调用,在最坏的情况下,xymodulus各自跨越两个页面且不重叠,而res也跨越两个页面且不重叠,并且系统调用的指令本身可能出现在一个新的页面中。这9个页面甚至可以只共享根级页面表,而第1、2、3、4级页面表则不共享——确保xymodulusres跨越非常、非常特殊的位置。请注意,res可能之前没有被读取过,因此它也需要加载页面并将这些页面标记为脏。这已经可以加载37个页面并将11个页面标记为脏。

为了避免混淆,分析器将突出显示新段中的第一条指令。

如果您想要对页面进行更精确的分析,请考虑使用带有RISC-Zero特定GDB stub的GDB。

https://github.com/l2iterative/gdb0

这个GDB stub提供了一些命令,用于查询当前周期数、已加载页面数和脏页面数。

许可证

请参阅LICENSE

依赖项

~14–26MB
~284K SLoC