Rust性能分析

 

Rust 性能分析

因为常见的性能瓶颈一般都是两类,CPU 和 I/O 。所以工具也基本面向这两类。

on-cpu 分析

  • cargo-flamegraph
  • perf

可以通过 Frame Pointer 和 DWARF 两种方式拿到函数的调用栈,默认是不启用的,通过下列方式启用 Frame Pointer

RUSTFLAGS="-C force-frame-pointers=yes" cargo build --release

因为Frame Pointer的保存和恢复需要引入额外的指令从而带来性能开销,所以Rust编译器,gcc编译器默认都是不会加入Frame Pointer的信息,需要通过编译选项来开启。

Frame Pointer

Frame Pointer是基于标记栈基址 EBP 的方式来获取函数调用栈的信息,通过 EBP 我们就可以拿到函数栈帧的信息,包括局部变量地址,函数参数的地址等。 在做 CPU profiling 的过程中,Frame Pointer 帮助进行函数调用栈的展开,具体原理是编译器会在每个函数入口加入如下的指令以记录调用函数的 EBP 的值

push ebp
mov	ebp, esp
sub	esp, N

并在函数结尾的时候加入如下指令以恢复调用函数的EBP

mov	esp, ebp
pop	ebp
ret

通过这种方式整个函数调用栈像一个被 EBP 串起来的链表,如下图所示: alt text

通过 EBP 调试程序就可以拿到完整的调用栈信息,进而进行调用栈展开。

perf

最简单的使用方式:

# 在 Cargo.toml 加入
[profile.release]
debug = true

# 执行
cargo build --release
perf record -g target/release/perf-test
perf report

火焰图

利用 perf 生成火焰图:

git clone https://github.com/brendangregg/FlameGraph
cd FlameGraph
sudo perf record --call-graph=dwarf mytest
sudo perf script | ./stackcollapse-perf.pl > out.perf-folded
./flamegraph.pl out.perf-folded > perf.svg

火焰图的纵轴代表了函数调用栈,横轴代表了占用CPU资源的比例,跨度越大代表占用的CPU资源越多,从火焰图中我们可以更直观的看到程序中CPU资源的占用情况以及函数的调用关系。

还可以使用 cargo-flamegraph 生成火焰图,但是对于异步 Async 代码来说,火焰图的效果不好,因为异步调度器和执行器几乎会出现在火焰图中每一块地方,看不出瓶颈所在。这个时候使用 perf 工具会更加清晰。

off-cpu 分析

Off-CPU 是指在 I/O、锁、计时器、分页/交换等被阻塞的同时等待的时间。

Off-CPU 的性能剖析通常可以在程序运行过程中进行采用链路跟踪来进行分析。还有就是使用 offcpu 火焰图进行可视化观察。

这里推荐的工具是 eBPF 的前端工具包 bcc 中的 offcputime-bpfcc 工具。 这个工具的原理是在每一次内核调用finish_task_switch()函数完成任务切换的时候记录上一个进程被调度离开CPU的时间戳和当前进程被调度到CPU的时间戳,那么一个进程离开CPU到下一次进入CPU的时间差即为Off-CPU的时间。

需要使用debug编译,因为offcputime-bpfcc依赖于frame pointer来进行栈展开,所以我们需要开启RUSTFLAGS=”-C force-frame-pointers=yes”的编译选项以便打印出用户态的函数栈。我们使用如下的命令获取Off-CPU的分析数据。

./target/debug/mytest & sudo offcputime-bpfcc -p `pgrep -nx mytest` 5

然后使用 火焰图工具将其生成 off-cpu 火焰图:

git clone https://github.com/brendangregg/FlameGraph
cd FlameGraph
sudo offcputime-bpfcc -df -p `pgrep -nx mytest` 3 > out.stacks
./flamegraph.pl --color=io --title="Off-CPU Time Flame Graph" --countname=us < out.stacks > out.svg

与On-CPU的火焰图相同,纵轴代表了函数调用栈,横轴代表了Off-CPU时间的比例,跨度越大代表Off-CPU的时间越长。

检查内存泄漏和不必要的内存分配

可以使用 Valgrind 工具来检查程序是否存在内存泄露,或者在关键的调用路径上存在不必要的内存分配。

有一个非常有用的 Rust 编译标志(仅在 Rust nightly 中可用)来验证数据结构有多大及其缓存对齐。

RUSTFLAGS=-Zprint-type-sizes cargo build --release

除了通常的 Cargo 输出之外,包括异步 Future 在内的每个数据结构都以相应的大小和缓存对齐方式打印出来。比如:

print-type-size type: `net::protocol::proto::msg::Data`: 304 bytes, alignment: 8 bytes
print-type-size     field `.key`: 40 bytes
print-type-size     field `.data_info`: 168 bytes
print-type-size     field `.payload`: 96 bytes

Rust 异步编程非常依赖栈空间,异步运行时和库需要把所有东西放到栈上来保证执行的正确性。 如果你的异步程序占用了过多的栈空间,可以考虑将其进行优化为平衡的同步和异步代码组合,把特定的异步代码隔离出来也是一种优化手段。

其他适合 Rust 性能剖析的工具介绍

除了 perf 和 火焰图 工具,下面还有一些 Rust 程序适用的工具。

Hotspot和Firefox Profiler是查看perf记录的数据的好工具。 Cachegrind和Callgrind给出了全局的、每个函数的、每个源线的指令数以及模拟的缓存和分支预测数据。 DHAT可以很好的找到代码中哪些部分会造成大量的分配,并对峰值内存使用情况进行深入了解。 heaptrack是另一个堆分析工具。 counts支持即席(Ad Hoc)剖析,它将eprintln!语句的使用与基于频率的后处理结合起来,这对于了解代码中特定领域的部分内容很有帮助。 Coz执行因果分析以衡量优化潜力。它通过coz-rs支持Rust。因果分析技术可以找到程序的瓶颈并显示对其进行优化的效果。

参考

  • https://rustmagazine.github.io/rust_magazine_2021/chapter_11/rust-profiling.html
  • https://github.com/iovisor/bcc
  • https://rustmagazine.github.io/rust_magazine_2021/chapter_12/rust-perf.html