HiPerf Annotate 工具
概述
HiPerf Annotate 是一个源代码注释工具,能够将性能分析信息直接添加到源代码和反汇编代码中。它帮助开发者通过显示每个函数、源代码行和汇编指令的执行时间统计,快速定位性能瓶颈。
功能特性
- 多格式支持:同时支持
perf.data(二进制)和perf.data.dump(文本)格式 - 支持平台:支持 Windows 平台
- 智能符号化:使用 LLVM 工具链(llvm-symbolizer/llvm-addr2line)进行精确的地址到源码映射
- 灵活过滤:支持按特定动态共享对象(Dynamic Shared Object,DSO)进行过滤分析
- 源代码注释:在源文件中添加内联性能注释
- 反汇编注释:生成带性能注释的反汇编代码,支持指令级性能分析
- DSO 大小阈值:可配置 DSO 大小阈值,跳过过大的库以提高性能
安装
前置条件
-
Python 3.8+
检查是否安装 python3。
python3 --version -
LLVM 工具链(可选安装)
- 系统安装 llvm-symbolizer/llvm-addr2line/llvm-objdump 工具,将 LLVM 工具路径配置到系统环境变量 PATH
- 检查是否安装 LLVM 工具
llvm-symbolizer --version llvm-addr2line --version llvm-objdump --version
快速开始
# 基本用法
python3 hiperf_annotate.py -i perf.data -s /path/to/source --sym_dir ./binary_cache
# 指定 LLVM 工具路径
python3 hiperf_annotate.py -i perf.data -s /path/to/source --ndk_path /path/to/ndk --sym_dir ./binary_cache
# 直接使用 dump 文件
python3 hiperf_annotate.py -i perf.data.dump -s /path/to/source --sym_dir ./binary_cache
# 生成反汇编注释
python3 hiperf_annotate.py -i perf.data -s /path/to/source --sym_dir ./binary_cache --add_disassembly
使用方法
命令行选项
| 选项 | 说明 | 必需 | 默认值 |
|---|---|---|---|
-i, --input |
输入文件(perf.data 或 perf.data.dump) | 是 | - |
-s, --source_dirs |
包含源文件的目录 | 否 | - |
--sym_dir |
包含符号文件的目录(可指定多个) | 否 | ./binary_cache |
--ndk_path |
LLVM 工具路径 | 否 | - |
--raw_period |
显示原始周期值而非百分比 | 否 | False |
--summary_width |
摘要文件的最大宽度 | 否 | 30 |
--dso |
仅分析指定 DSO 的数据 | 否 | - |
-o, --output |
注释文件的输出目录 | 否 | annotated_files |
--add_disassembly |
生成反汇编注释 | 否 | False |
--disassembly_output_dir |
反汇编输出目录 | 否 | annotated_disassembly |
--dso_size_threshold |
DSO 大小阈值,超过阈值的 DSO 不处理 | 否 | 1G |
使用示例
示例 1:基本注释
# 使用性能数据注释源文件
python3 hiperf_annotate.py \
-i perf.data \
-s src/ \
--sym_dir binary_cache/
示例 2:DSO 过滤
# 仅分析特定库
python3 hiperf_annotate.py \
-i perf.data \
-s src/ \
--sym_dir binary_cache/ \
--dso libhilog.so
示例 3:原始周期值
# 显示原始周期值而非百分比
python3 hiperf_annotate.py \
-i perf.data \
-s src/ \
--sym_dir binary_cache/ \
--raw_period
示例 4:自定义输出目录
# 指定自定义的输出目录
python3 hiperf_annotate.py \
-i perf.data \
-s src/ \
--sym_dir binary_cache/ \
-o results/
示例 5:生成反汇编注释
# 生成带性能注释的反汇编代码
python3 hiperf_annotate.py \
-i perf.data \
-s src/ \
--sym_dir binary_cache/ \
--add_disassembly
示例 6:设置 DSO 大小阈值
# 跳过大于512MB的DSO
python3 hiperf_annotate.py \
-i perf.data \
-s src/ \
--sym_dir binary_cache/ \
--dso_size_threshold 512M
输出说明
目录结构
annotated_files/ # 源代码注释输出目录
├── summary # 摘要报告
├── binary_cache.txt # 二进制文件索引缓存
├── run.log # 运行日志
└── [源代码目录结构]
└── path/
└── to/
└── source/
├── main.cpp # 带注释的源文件
├── utils.cpp
└── ...
annotated_disassembly/ # 反汇编输出目录(如果启用 --add_disassembly)
└── [DSO名称目录]
└── [函数名].asm # 带注释的反汇编文件
摘要报告
摘要文件包含多层次的性能统计:
total period: 1000
=== DSO Summary ===
Total Self DSO
30.00% 5.00% /system/lib64/libhilog.so
20.00% 15.00% /system/lib64/libapp.so
=== File Summary ===
Total Self Source File
50.00% 10.00% /path/to/source/main.cpp
30.00% 5.00% /path/to/source/utils.cpp
=== Function/Line Summary in /path/to/source/main.cpp ===
Total Self Function/Line
40.00% 20.00% main(StartLine 10)
10.00% 10.00% main line 15
5.00% 5.00% main line 16
理解 Total 与 Self
核心概念:
- Total(累加周期):该函数/行执行的总时间,包括其调用的所有子函数的执行时间
- Self(自身周期):该函数/行本身的执行时间,不包括子函数的执行时间
关键关系:
Total = Self + 所有子函数的 Self
为什么 Total 百分比加起来不等于 100%?
这是正常且符合预期的,因为存在重复计数问题,参考以下示例代码进行说明:
int main() { // 函数A
int result = calculate(); // 调用函数B
printf("result: %d\n", result); // 函数A自己的代码
return 0;
}
int calculate() { // 函数B
int sum = 0;
for (int i = 0; i < 100; i++) { // 函数B自己的代码
sum += i;
}
return sum;
}
假设性能采样结果:
main()函数:Total=100%, Self=20%calculate()函数:Total=80%, Self=80%
解释:
main().Total=100%:表示程序运行总时间中,100% 的时间花在main()及其所有子调用上main().Self=20%:表示只有 20% 的时间花在main()自己的代码上(printf()等)calculate().Total=80%:表示 80% 的时间花在calculate()及其子调用上calculate().Self=80%:表示 80% 的时间花在calculate()自己的代码上
验证公式:
main().Total = main().Self + calculate().Self
100% = 20% + 80% ✓ 正确
为什么 Total 加起来超过 100%?
main().Total + calculate().Total = 100% + 80% = 180%
这是正常的,因为:
main().Total包含了calculate()的所有时间calculate().Total也包含了它自己的所有时间calculate()的时间被重复计算了两次
正确的理解方式:
- Total 列:查看该函数在调用链中的位置,越靠上层的函数 Total 越大
- Self 列:查看该函数本身的执行时间,所有函数的 Self 加起来等于 100%
实际应用场景:
- 定位热点函数:看 Total 列,找到 Total 值大的函数(说明该函数及其子调用消耗大量时间)
- 定位热点代码:看 Self 列,找到 Self 值大的函数(说明该函数本身消耗大量时间)
- 优化策略:
- 如果某函数 Total 大但 Self 小 → 说明其子函数是热点,应该优化子函数
- 如果某函数 Self 大 → 说明该函数本身是热点,应该优化该函数的算法
源文件注释
源文件注释示例:
/* [file] Total 100.00%, Self 100.00% */#include <stdio.h>
/* [func] Total 85.00%, Self 85.00% */int calculate() {
int result = 0;
/* Total 50.00%, Self 50.00% */ for (int i = 0; i < 100; i++) {
/* Total 35.00%, Self 35.00% */ result += i;
}
return result;
}
/* [func] Total 100.00%, Self 10.00% */int main() {
/* Total 90.00%, Self 5.00% */ int result = calculate();
/* Total 10.00%, Self 10.00% */ printf("result: %d\n", result);
return 0;
}
源文件注释说明:
[file]:文件级别的统计信息(仅在文件第一行显示)[func]:函数级别的统计信息(在函数起始行显示)Total X%, Self Y%:总周期和自身周期的百分比
反汇编文件注释
当启用 --add_disassembly 选项时,会生成带性能注释的反汇编代码:
反汇编文件注释示例:
/* Function: calculate */
/* 10.00% */ ldr w0, [sp, #24]
反汇编注释说明:
- 第一行是函数名注释:
/* Function: 函数名 */ - 每条指令前面的注释显示该指令的执行时间占比
- 百分比表示该指令执行时间占总执行时间的比例
- 无百分比数值的指令表示该指令没有采样到
架构设计
hiperf_annotate.py
├── DumpFileParser # 解析 perf.data.dump 格式
├── HiperfAddr2Line # 地址到源代码行转换
├── HiperfReadElf # ELF文件信息提取
├── HiperfBinaryFinder # 二进制文件查找
├── HiperfObjdump # 反汇编工具封装
├── SourceFileAnnotator # 源代码注释控制器
├── DisassemblyAnnotator # 反汇编注释控制器
└── Period, DsoPeriod, FilePeriod, Symbol, Sample # 数据结构
故障排除
问题:找不到 llvm-symbolizer
错误信息:
ERROR: Cannot find llvm-symbolizer or llvm-addr2line. Please install LLVM toolchain or specify --ndk_path path.
解决方案:
- 系统安装 llvm-symbolizer/llvm-addr2line 工具,检查 llvm-symbolizer/llvm-addr2line 是否在环境变量中,确保命令行工具可访问
- 使用
--ndk_path参数指定 LLVM 工具路径
问题:源文件找不到
警告信息:
WARNING: Can't find source file: /path/to/source/main.cpp
解决方案:
- 确保使用
-s参数指定了正确的源代码目录 - 检查源文件路径是否正确
- 确保源文件存在于指定目录中
问题:Build ID 不匹配
警告信息:
WARNING: Build ID mismatch for /path/to/libxxx.so: expected xxx, got yyy
解决方案:
- 确保指定 sym_dir 目录下的符号文件是正确的版本
- 检查 perf.data 和符号文件是否来自同一构建版本
问题:没有 .debug_line section
警告信息:
WARNING: Binary file doesn't contain .debug_line section.
解决方案:
- 使用带调试信息的符号文件(未 stripped)
问题:DSO被跳过
警告信息:
WARNING: Skipping large DSO: libc.so (size: 2147483648 bytes > threshold: 1073741824 bytes)
解决方案:
- 这不是错误,是性能优化措施
- 如果需要分析该DSO,增加
--dso_size_threshold值
汇总信息:
Skipped X large DSOs, Y DSOs not found, Z DSOs with build ID mismatch
说明:
- X:跳过的大型 DSO 数量
- Y:未找到的 DSO 数量
- Z:Build ID 不匹配的 DSO 数量
问题:反汇编生成失败
警告信息:
WARNING: Cannot find llvm-objdump. Please install LLVM toolchain or specify --ndk_path path.
解决方案:
- 系统安装 llvm-objdump 工具,检查 llvm-objdump 是否在环境变量中,确保命令行工具可访问
- 使用
--ndk_path参数指定 LLVM 工具路径
许可证
Apache License 2.0
详见 LICENSE 文件。
联系方式
如有问题、疑问或贡献,请联系 HiPerf 开发团队。
祝性能分析愉快!🚀