查看堆栈信息
查看堆栈信息有三种方法:
使用 gdb attach 到进程上。
使用 gstack 导出栈信息。
使用 pstack 到处栈信息。
ASan
TL;DR
添加 编译 和 链接 参数:
-O1 -g -fsanitize=address -fno-omit-frame-pointer
添加环境变量
export ASAN_SYMBOLIZER_PATH=/path/to/llvm-symbolizer export ASAN_OPTIONS=detect_leaks=1
执行程序
如果在 macos 上执行程序时出现了 [1]
则可以通过添加环境变量 |
使用 ASan 构建程序
ASan(AddressSanitizer) 是一个内存错误检测工具。
使用方式为:
添加编译和链接参数:
-O1 -g -fsanitize=address -fno-omit-frame-pointer
设置环境变量 ASAN_SYMBOLIZER_PATH 以指向 llvm-symbolizer 文件
执行程序
如果希望发现错误后仍然运行,则需要:
添加编译链接参数:
-fsanitize-recover=address
添加环境变量
ASAN_OPTIONS=halt_on_error=0
ASan 有一些运行程序前设置的 Flags
ASan 的一些错误解释可以在 C/C++ 擦除器 | 用于运行时 bug 检测的检测代码 中找到。
LeakSanitizer
LeakSanitizer 默认集成到了 ASan 中。可用于检测内存泄漏。要使用 LeakSanitizer,需要添加运行时参数:
ASAN_OPTIONS=detect_leaks=1
由于 LLVM 自身的问题,可能会对某些函数进行误报。这是可以通过设置 LSAN 的参数来忽略某些函数。例如:
# suppr.txt
leak:fetchInitializingClassList
leak:cache_t::insert
然后添加运行参数:
export LSAN_OPTIONS=suppressions=`pwd`/suppr.txt
即可。
ThreadSanitizer
TSan(ThreadSanitizer) [2] 用于检测程序中的条件竞争。要使用 TSan,需要:
添加编译参数
-O2 -g -fsanitize=thread
TSan 和 ASan 是互斥的 |
TSan 同样有一些运行时设置的 Flags
TSan 不支持 libc/libc++ 静态链接 |
MemorySanitizer
MSan 用于检测内存未初始化的问题。要使用 MSan,需要:
添加编译和链接参数:
-fsanitize=memory -fPIE -pie -fno-omit-frame-pointer -fsanitize-memory-track-origins
设置环境变量 MSAN_SYMBOLIZER_PATH 以指向 llvm-symbolizer 文件
MSan 要求使用 MSan 构建程序中的所有代码 |
realloc 导致的内容错误
在进行 benchmark 时碰到了下面的错误:
bench_big_file-48da6c631385e586: malloc.c:2617: sysmalloc: Assertion `(old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0)' failed.
benchmark 默认是打开 asan 了的,但是依然碰到这个问题。说明内存读写没有出现问题。经过调试,发现是由于 array 在 grow 过程中使用 realloc 分配内存,但是 capacity 并没有更新。从而导致了这个问题。
UBSan
UBSan 用于探测未定义行为。最简单的形式是添加编译和链接参数 -fsanitize=undefined
对于 Rust 而言,启用的方式为: [3]
CFLAGS="-fsanitize=address,undefined -fno-sanitize-recover=all" cargo build
TIPS: UBSan 可以和 ASan 一同启用。
对于更复杂的 UBSan,参见 UndefinedBehaviorSanitizer
sysprof
sysprof 是 一个系统性能监控工具,可以用来分析进程的详细信息。sysprof 是一个图形应用,但是也提供了 sysprof-cli 在终端使用。
缺点:
需要超级用户权限
valgrind
valgrind 包含了一系列用于优化的工具。
callgrind
valgrind --tool=callgrind --separate-threads=yes ./dsmaster --terminal -l trace
gprof2dot -f callgrind -n10 -s callgrind.out.146758-02 > 1.dot
dot -Tpng valgrind.dot -o valgrind.png
kcachegrind
gprof
gprof 是 gcc 自带的性能分析工具。其通过在代码中插入代码片段完成对性能的分析。
缺点:
进程需要能够正常结束。因而无法对 deamon 服务进行分析。
进程需要能够容忍 SIGPROF 信号
gprof 只能分析用户态时间
不能用于多线程
不能分析函数指针
gprof 的使用方式为:
添加编译参数 -pg
添加链接参数 -pg
执行程序使其正常结束以生成 gmon.out 文件
生成图形化结果:
gprof /path/to/execute | gprof2dot -n0 -e0 | dot -Tpng -o output.png
gperftools
gperftools 和 gprof 优缺点相似。使用方式为:
链接 profiler 库
执行 CPUPROFILE=/tmp/profile ./myprogram
获取结果:
pprof ./test_capture test_capture.prof --pdf > prof.pdf
TIPS: gperftools 中的 pprof 工具可以读取 perf 数据文件,因此可以将它们一起使用。
perf
如果 perf 出现 Rust 调用栈错误,可能的解决方式为添加 --call-graph dwarf
编译参数。
Bytehound
Bytehound 是一个内存分析工具。其通过替换 malloc 的方式对程序进行分析。能够用来分析堆栈使用和内存泄露信息。
todo
使用 GCC 分析堆栈使用
GCC 提供了以下编译标志用来协助分析堆栈使用:
标志 | 作用 |
---|---|
-fstack-usage | 生成一个后缀名为 |
编译器优化
SROA(Scalar Replacement of Aggregates)
SROA 优化用于在简单的情况下对结构体进行优化。当结构体仅用作局部变量时,SROA 会尝试将部分字段使用常量或者局部变量替代,而结构体中的此字段被删除。
MacOS
在 MacOS 上没有 perf 命令,可以通过两个方式解决:
使用 instruments 指令。例如:
instruments -t "Time Profiler" ./stm32flash -m 8n1 -b 115200 -w xx.hex -S 0x000c00:9000 -v /dev/tty.usbserial-14520
然后直接打开文件即可:
open instrumentscli0.trace
使用 dtrace 命令
# 捕获进程 pid == 12345 的信息。 dtrace -x ustackframes=100 -n 'profile-97 /pid == 12345/ { @[ustack()] = count(); } tick-60s { exit(0); }' -o out.user_stacks stackcollapse.pl out.kern_stacks > out.kern_folded # 生成火焰图 flamegraph.pl out.kern_folded > kernel.svg
分离调试信息
$ gcc -g -shared -o libtest.so libtest.c (1)
$ objcopy --only-keep-debug libtest.so libtest.debug (2)
$ objcopy --add-gnu-debuglink=libtest.debug libtest.so (3)
$ strip libtest.so
通过
-g
选项保留 debuginfo。将 libtest.so 中的调试信息拷贝到 libtest.debug 中。
告诉 gdb 调试时从 libtest.debug 中获取调试信息。
build id
如果链接时添加参数 -build-id
,则 ld 会在文件中添加一个 note.gnu.build-id
,通过 objdump 可以获取它的值:
$ objdump -s -j .note.gnu.build-id /bin/ls
/bin/ls: file format elf32-i386
Contents of section .note.gnu.build-id:
8048168 04000000 14000000 03000000 474e5500 ............GNU.
8048178 c6fd8024 2a11673c 7c6a5af6 2c65b1b5 ...$*.g<|jZ.,e..
8048188 d7e13fd4
gdb 会安装下面的顺序查找 debuginfo:
查找
[debug-file-directory]/.build-id/xx/yyyyyy.debug
。debug-file-directory 是 gdb 的环境变量。可以通过
show debug-file-directory
命令获取。xx 是十六进制 hash 的前两位,yyyyy 是剩余部分。例如对于上面的例子而言,xx 是 c6,而 yyyyy 是 fd80242a11673c7c6a5af62c65b1b5d7e13fd4。最后总的路径为
/usr/lib/debug/.build-id/c6/fd80242a11673c7c6a5af62c65b1b5d7e13fd4.debug
在当前路径下查找 debuglink 指向的文件。
在 ./.debug 路径下查找 debuglink 指向的文件。
在 [debug-file-directory]/[file-path] 中查找文件。例如
/usr/lib/libfoo.so
对应的路径为/usr/lib/debug/usr/lib/libfoo.so
。
添加自定义内容到 elf
使用 objcopy 可以将自定义内容添加到 elf 中。例如将 commit 保留下来:
build:
echo $(shell git rev-parse HEAD) > .version
objcopy --add-section .foo.commit_id=.version libfoo.so
评估软件缓存性能
部分软件(例如 slab 分配器)可能需要考虑缓存问题,频繁的内存失效会极大程度上影响程序的性能。欲评估软件缓存性能,可以使用 perf 或者 cachegrind。
使用 perf 评估缓存性能[1]
如要大致了解程序使用系统缓存的情况,可以使用下面的命令:
# perf stat -e task-clock,cycles,instructions,cache-references,cache-misses ./a.out
程序结束后,perf 将会生成类似下面的输出:
Performance counter stats for './a.out': 229.872935 task-clock # 0.996 CPUs utilized 626,676,991 cycles # 2.726 GHz 525,543,766 instructions # 0.84 insns per cycle 18,587,219 cache-references # 80.859 M/sec 6,605,955 cache-misses # 35.540 % of all cache refs 0.230761764 seconds time elapsed
上述的程序相对较短:只执行了 525,543,766 条指令,一共需要 626,676,991 个处理器周期。
\(\frac{缓存未命令中数}{指令数}\) 的值越低越好。本示例中的值为 \(\frac{626,676,991}{525,543,766}=1.25\%\)。由于 RAM 内存和缓存访问成本差异很大,因此此比例的细微变动也能够显著影响性能。如果未命中比率超过 5%,则需要进一步调查。
当 dwarf 信息可以访问时,可以获取缓存未命中的代码位置,可以使用 perf record
而不是 perf state
来收集这些信息:
$ perf record -e cache-misses ./a.out
命令执行完毕后,信息被收集到 perf.data
中:
[ perf record: Woken up 1 times to write data ] [ perf record: Captured and wrote 0.048 MB perf.data (~2093 samples) ]
使用 perf report
可以查看详细信息:
$ perf report --stdio
# ========
# captured on: Wed Oct 2 14:38:59 2013
# hostname : dhcp129-131.rdu.redhat.com
# os release : 3.10.0-31.el7.x86_64
# perf version : 3.10.0-31.el7.x86_64.debug
# arch : x86_64
# nrcpus online : 8
# nrcpus avail : 8
# cpudesc : Intel(R) Core(TM) i7-3740QM CPU @ 2.70GHz
# cpuid : GenuineIntel,6,58,9
# total memory : 7859524 kB
# cmdline : /usr/bin/perf record -e cache-misses ./a.out
# event : name = cache-misses, type = 0, config = 0x3, config1 = 0x0, config2 =
# HEADER_CPU_TOPOLOGY info available, use -I to display
# HEADER_NUMA_TOPOLOGY info available, use -I to display
# pmu mappings: cpu = 4, software = 1, tracepoint = 2, uncore_cbox_0 = 6, uncore
# ========
#
# Samples: 888 of event 'cache-misses'
# Event count (approx.): 6576993
#
# Overhead Command Shared Object Symbol
# ........ ............ ................. ............................
#
85.19% a.out a.out [.] main
11.20% a.out [kernel.kallsyms] [k] clear_page_c_e
2.23% a.out a.out [.] checkSTREAMresults
0.62% a.out [kernel.kallsyms] [k] _cond_resched
0.20% a.out [kernel.kallsyms] [k] trigger_load_balance
0.13% a.out [kernel.kallsyms] [k] task_tick_fair
0.13% a.out [kernel.kallsyms] [k] free_pages_prepare
0.11% a.out [kernel.kallsyms] [k] perf_pmu_disable
0.09% a.out [kernel.kallsyms] [k] tick_do_update_jiffies64
0.09% a.out [kernel.kallsyms] [k] __acct_update_integrals
0.00% a.out [kernel.kallsyms] [k] flush_signal_handlers
0.00% a.out [kernel.kallsyms] [k] perf_event_comm_output
#
# (For a higher level overview, try: perf report --sort comm,dso)
#
如果需要更详细的信息,可以使用 perf annotate
命令:
$ perf annotate
main
│ movsd %xmm0,(%rsp) ▒
│ xchg %ax,%ax ▒
│ #ifdef TUNED ▒
│ tuned_STREAM_Add(); ▒
│ #else ▒
│ #pragma omp parallel for ▒
│ for (j=0; j<N; j++) ▒
│ c[j] = a[j]+b[j]; ▒
1.96 │2a0: movsd 0x24868e0(%rax),%xmm0 ▒
11.17 │ add $0x8,%rax ▒
2.21 │ addsd 0x15444d8(%rax),%xmm0 ▒
13.74 │ movsd %xmm0,0x6020d8(%rax) ▒
│ times[2][k] = mysecond(); ◆
│ #ifdef TUNED ▒
│ tuned_STREAM_Add(); ▒
│ #else ▒
│ #pragma omp parallel for ▒
│ for (j=0; j<N; j++) ▒
3.56 │ cmp $0xf42400,%rax ▒
│ ↑ jne 2a0 ▒
│ c[j] = a[j]+b[j]; ▒
│ #endif ▒
Press 'h' for help on key bindings
使用 Cachegrind 评估缓存性能[2]
要使用此工具,需要在 Valgrind 命令行上指定 --tool=cachegrind
:
$ valgrind --tool=cachegrind ls -l
执行完成后,Cachegrind 会类似下面的统计信息:
==31751== I refs: 27,742,716 ==31751== I1 misses: 276 ==31751== L2 misses: 275 ==31751== I1 miss rate: 0.0% ==31751== L2i miss rate: 0.0% ==31751== ==31751== D refs: 15,430,290 (10,955,517 rd + 4,474,773 wr) ==31751== D1 misses: 41,185 ( 21,905 rd + 19,280 wr) ==31751== L2 misses: 23,085 ( 3,987 rd + 19,098 wr) ==31751== D1 miss rate: 0.2% ( 0.1% + 0.4%) ==31751== L2d miss rate: 0.1% ( 0.0% + 0.4%) ==31751== ==31751== L2 misses: 23,360 ( 4,262 rd + 19,098 wr) ==31751== L2 miss rate: 0.0% ( 0.0% + 0.4%)
除了摘要信息外,Cachegrind 还会将信息以人类可读的形式保存到 cachegrind.out.pid
文件中。使用附带的 cg_annotate
工具可以更好地分析此文件。
使用 mallopt 探测内存问题
GLIBC 提供的 malloc 可以通过环境变量或者 mallopt 来更改部分行为。具体值如下:
mallopt | 环境变量 | 用途 | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
M_CHECK_ACTION | MALLOC_CHECK_ |
| ||||||||||||||
M_PERTURB | MALLOC_PERTURB_ | 使用指定值初始化分配的内存(使用 memset) |