首发|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 ebpmov    ebp, espsub    esp, N

并在函数结尾的时候退出如下指令以复原调用函数的EBP

mov    esp, ebppop    ebpret

通过这种形式整个函数调用栈像一个被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...