共计 5532 个字符,预计需要花费 14 分钟才能阅读完成。
首发|RustMagazine
简介
程序的性能剖析是一个很广很深的话题,有各种各样的工具来对不同的指标进行测试剖析。本文次要介绍如何用 profiling 工具对 Rust 程序进行 On-CPU 和 Off-CPU 的性能剖析,以及如何绘制火焰图对后果测试进行可视化解决。
On-CPU 性能剖析
On-CPU 的性能剖析为了找出占用 CPU 工夫多的工作或者函数,进而找出程序的性能瓶颈。这里次要介绍 perf 工具,perf 是 Linux 提供的命令,也叫 perf_events,它属于 Linux kernel,在 tools/perf 目录下。perf 提供了弱小的性能包含监测 CPU performance counters, tracepoints, kprobes 和 uprobes 等。这里咱们应用 perf 的 CPU profiling 性能。因为 perf 会拿到零碎的信息,所以运行须要 root 权限。perf 做 On-CPU 性能剖析的原理是以一个指定的频率对 CPU 进行采样,进而拿到正在 CPU 上运行的指令乃至整个函数调用栈的快照,最初对采样的数据分析,比如说在 100 次采样中有 20 次在运行 A 指令或者 A 函数,那么 perf 就会认为 A 函数的 CPU 使用率为 20%。
上面咱们应用一个简略的程序来展现如何用 perf 来进行 On-CPU 的性能剖析,须要应用 debug 编译,程序如下:
fn test2() {
for _ in 0..200000 {()
}
}
fn test1() {
for _ in 0..100000 {()
}
test2();}
fn main() {
for _ in 0..10 {test1();
}
}
咱们在 test1() 函数和 test2() 函数中别离退出了一个段循环来耗费 CPU 资源,在 test2() 中咱们循环了 200000 次是在 test1() 中的两倍。咱们应用如下 perf 命令来做 CPU profiling:
$ sudo perf record --call-graph=dwarf ./target/debug/mytest
采样的数据默认会存到 perf.data 文件中。参数 –call-graph 的目标是开启函数调用栈的记录,这样在 profiling 的后果中能够打印出残缺的函数调用栈。目前 perf 反对 fp(frame pointer), dwarf(DWARF’s CFI – Call Frame Information) 和 lbr(Hardware Last Branch Record facility) 三种形式来获取函数调用栈。稍后咱们会简略介绍 fp 和 dwarf 的原理。因为 Rust 编译器默认生成了 dwarf 格局的调试信息,咱们能够间接应用 –call-graph=dwarf。咱们能够应用如下命令来读取 profiling 的后果:
$ sudo perf report --stdio
这个命令会默认读取 perf.data 中的数据并格式化输入。命令的输入如下,因为输入很长,局部无关信息被省略掉,
# Children Self Command Shared Object Symbol
# ........ ........ ....... ................. .........................................................................................................
#
77.57% 0.00% mytest mytest [.] _start
|
---_start
__libc_start_main
main
......
mytest::main
mytest::test1
|
|--52.20%--mytest::test2
| |
| --46.83%--core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next
| |......
|
--23.87%--core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next
|......
|......
从输入能够看出整个测试程序占用了 77.57% 的 CPU 资源,在 mytest::test1 函数中有 23.87% 的工夫在做 for 循环,有 52.20% 的工夫被调用的 mytest::test2 函数占用,而后在 mytest::test2 函数中,有 45.32% 工夫在做 for 循环,剩下的工夫是一些其余的开销。profiling 的后果根本体现了咱们测试程序 CPU 占用率的理论状况。咱们在 mytest::test2 函数中 for 循环次数是 mytest::test1 函数中两倍,相应的咱们也看到了在 CPU 占用率上也简直是两倍的关系。
火焰图
火焰图是对 profiling 进行可视化解决的一种形式,从而更直观地展现程序 CPU 的应用状况,通过如下命令能够生成火焰图
$ 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
能够应用 Chrome 浏览器关上 perf.svg,生成的火焰图如下
火焰图的纵轴代表了函数调用栈,横轴代表了占用 CPU 资源的比例,跨度越大代表占用的 CPU 资源越多,从火焰图中咱们能够更直观的看到程序中 CPU 资源的占用状况以及函数的调用关系。
Frame Pointer 和 DWARF
前文有提到 Frame Pointer 和 DWARF 两种形式拿到函数的调用栈,这里做一个简略的介绍。
Frame Pointer 是基于标记栈基址 EBP 的形式来获取函数调用栈的信息,通过 EBP 咱们就能够拿到函数栈帧的信息,包含局部变量地址,函数参数的地址等。在做 CPU profiling 的过程中,fp 帮忙函数调用栈的开展,具体原理是编译器会在每个函数入口退出如下的指令以记录调用函数的 EBP 的值
push ebp
mov ebp, esp
sub esp, N
并在函数结尾的时候退出如下指令以复原调用函数的 EBP
mov esp, ebp
pop ebp
ret
通过这种形式整个函数调用栈像一个被 EBP 串起来的链表,如下图所示
这样调试程序就能够拿到残缺的调用栈信息,进而进行调用栈开展。
因为 Frame Pointer 的保留和复原须要引入额定的指令从而带来性能开销,所以 Rust 编译器,gcc 编译器默认都是不会退出 Frame Pointer 的信息,须要通过编译选项来开启。Rust 编译器退出 Frame Pointer 的选项如下
$ RUSTFLAGS="-C force-frame-pointers=yes" cargo build
退出 Frame Pointer 的信息后就能够通过 –call-graph=fp 来打印函数的调用栈。
DWARF 是被宽泛应用的调试格局,Rust 编译器默认退出了 DWARF 调试信息,DWARF 格局提供了各种调试信息,在帮忙函数调用栈开展方面,编译器会插入 CFI(Call Frame Information) 指令来标记 CFA(Canonical Frame Address),CFA 指的是调用函数在 call 被调函数前 ESP 的地址。通过 CFA 再加上事后生成的调试信息,就能够解析出残缺的函数调用栈信息。
Off-CPU 性能剖析
Off-CPU 性能剖析与 On-CPU 性能剖析是互补的关系。Off-CPU 性能剖析是为了剖析过程花在期待上的工夫,期待包含被 I / O 申请阻塞,期待锁,期待 timer,等等。有很多能够做 Off-CPU 性能剖析的工具,这里咱们应用 eBPF 的前端工具包 bcc 中的 offcputime-bpfcc 工具。这个工具的原理是在每一次内核调用 finish_task_switch() 函数实现工作切换的时候记录上一个过程被调度来到 CPU 的工夫戳和以后过程被调度到 CPU 的工夫戳,那么一个过程来到 CPU 到下一次进入 CPU 的时间差即为 Off-CPU 的工夫。为了模仿 Off-CPU 的场景咱们须要批改一下咱们的测试程序,须要应用 debug 编译,因为 offcputime-bpfcc 依赖于 frame pointer 来进行栈开展,所以咱们须要开启 RUSTFLAGS=”-C force-frame-pointers=yes” 的编译选项,程序如下:
use std::io::Read;
fn test1() {std::thread::sleep(std::time::Duration::from_nanos(200));
}
fn test2() {let mut f = std::fs::File::open("./1.txt").unwrap();
let mut buffer = Vec::new();
f.read_to_end(&mut buffer).unwrap();}
fn main() {
loop {test1();
test2();}
}
程序中一共有两种会导致过程被调度出 CPU 的工作,一个是 test1() 函数中的 sleep(),一个是在 test2() 函数中的读文件操作。这里咱们须要开启 Frame Pointer 编译选项以便打印出用户态的函数栈。咱们应用如下的命令获取 Off-CPU 的剖析数据,
$ ./target/debug/mytest &
$ sudo offcputime-bpfcc -p `pgrep -nx mytest` 5
.......
b'finish_task_switch'
b'preempt_schedule_common'
b'_cond_resched'
b'copy_page_to_iter'
b'generic_file_buffered_read'
b'generic_file_read_iter'
b'ext4_file_read_iter'
b'new_sync_read'
b'vfs_read'
b'ksys_read'
b'__x64_sys_read'
b'do_syscall_64'
b'entry_SYSCALL_64_after_hwframe'
b'__libc_read'
b'std::io::read_to_end::hca106f474265a4d9'
b'std::io::Read::read_to_end::h4105ec7c4491a6c4'
b'mytest::test2::hc2f1d4e3e237302e'
b'mytest::main::h9ce3eef790671359'
b'core::ops::function::FnOnce::call_once::hac091ad4a6fe651c'
b'std::sys_common::backtrace::__rust_begin_short_backtrace::h071a56f0a04107d5'
b'std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hc491d6fbd79f86bd'
b'std::rt::lang_start_internal::h73711f37ecfcb277'
b'[unknown]'
- mytest (179236)
52
b'finish_task_switch'
b'schedule'
b'do_nanosleep'
b'hrtimer_nanosleep'
b'common_nsleep'
b'__x64_sys_clock_nanosleep'
b'do_syscall_64'
b'entry_SYSCALL_64_after_hwframe'
b'clock_nanosleep'
- mytest (179236)
1093
这里只截取了最初两个函数栈,输入显示有 52ms 的 Off-CPU 工夫在期待文件读取的 syscall,有 1092ms 的 Off-CPU 工夫在期待 sleep。
火焰图
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 的工夫越长。
总结
本文介绍了如何对 Rust 程序进行 On-CPU 和 Off-CPU 的性能剖析,两者相结合就实现了对一个程序 100% 运行工夫的剖析。至此咱们对程序的性能有了初步的了解,进而能够通过更加具体的工具和编写 profiling 程序来对咱们关怀的点进行更深刻的剖析。
参考援用
https://www.brendangregg.com/…
https://www.brendangregg.com/…
https://en.wikipedia.org/wiki…