1. 为什么你写的“-O2”程序,有时比“-O3”还快?
在某次嵌入式图像处理模块的性能调优中,我遇到一个典型反直觉现象:用-O3编译的边缘检测算法,在 ARM Cortex-A53 平台上实测耗时反而比-O2多出 14%。不是优化级别越高越好吗?当时团队里有位老工程师直接把编译命令里的-O3改成-O2,再加了-fno-tree-vectorize,结果帧率立刻从 23.6 fps 跳到 27.1 fps——而且内存访问抖动明显降低。
这件事让我重新翻开了 GCC 的gcc -Q --help=optimizers输出,也终于明白:GCC 的优化选项不是开关盒,而是一张精密调控的调音台。它不只决定“要不要优化”,更决定“对哪类代码路径激进优化”“在什么代价下让步”“如何与硬件微架构博弈”。比如-O3默认启用的自动向量化(auto-vectorization),在数据未对齐、分支预测失败率高或缓存局部性差的场景下,反而会引入额外的 shuffle 指令、寄存器溢出和 cache miss,把 CPU 流水线堵得比-O2还满。
这正是本文要拆解的核心:GCC 优化选项的本质,是开发者与编译器之间的一份隐式契约——你告诉它你的目标(速度/体积/调试友好),它基于你提供的源码结构、目标架构特性、甚至你没明说的运行时约束,动态生成最匹配的指令序列。
如果你只记口诀“-O2 是默认推荐”,却不知道-O2在 x86_64 上默认禁用-ftree-vectorize,而在 aarch64 上却默认开启;如果你不清楚-funroll-loops在循环体含函数调用时可能让代码膨胀 300% 却提速不到 2%;如果你没验证过-flto在多文件项目中因跨翻译单元分析带来的链接时长激增……那你就不是在用编译器,而是在赌编译器的运气。
本文面向三类人:
- 正在为线上服务延迟抖动发愁的后端开发者(你会看到
-fno-semantic-interposition如何让dlsym查找快 3 倍); - 调试内核模块时被内联函数搞晕的驱动工程师(
-fno-inline-functions-called-once是你的救命稻草); - 为 MCU 内存紧张焦头烂额的嵌入式工程师(
-Os和-Oz的真实体积差异,远不止文档里写的那几行)。
我们不罗列所有 200+ 个优化开关,而是聚焦12 个真正影响交付质量的核心选项,结合汇编输出、perf 火焰图、实际 benchmark 数据,告诉你:每个选项在什么场景下该开、该关、该配什么参数,以及——当 GCC 文档说“此选项默认启用”时,它到底在替你做了什么危险的假设。
2.-O级别背后的决策树:从源码到汇编的四层过滤逻辑
GCC 的-O级别(-O0,-O1,-O2,-O3,-Os,-Oz)不是简单叠加开关,而是一套分层激活的优化策略组合。理解它的关键,在于看清 GCC 如何将 C 源码抽象为四层中间表示(IR),并在每层施加不同粒度的变换:
2.1 GIMPLE 层:语义等价的“安全区”优化
这是最底层的三地址码表示,所有变量名被替换为 SSA(静态单赋值)形式的虚拟寄存器(如.D.1234)。在此层进行的优化,严格保证语义等价性——即无论输入数据如何,优化前后程序行为完全一致(忽略浮点精度等极少数例外)。
-O1启用的核心 GIMPLE 优化包括:--param max-gcse-memory=134217728:全局公共子表达式消除(GCSE)的最大内存预算(128MB),防止编译器在超大函数中卡死;-fgcse:启用 GCSE,将a = b + c; d = b + c;合并为tmp = b + c; a = tmp; d = tmp;;-fthread-jumps:将条件跳转后的重复代码块合并,减少指令缓存压力。
提示:
-O1对嵌入式小资源设备最友好——它几乎不增加代码体积,却能消除大量冗余计算。我在某款 32KB Flash 的传感器固件中,仅用-O1就让主循环周期缩短了 18%,因为编译器把 ADC 采样后的 5 次位运算合并成了 2 条指令。
2.2 RTL 层:面向目标架构的“硬件适配”优化
当 GIMPLE 被转换为 RTL(Register Transfer Language)后,优化开始与 CPU 微架构深度耦合。此时 GCC 不再只看“代码逻辑”,而是思考“这条指令在 Intel Skylake 上需要几个周期”“这个 load 操作能否被 CPU 的 store-to-load forwarding 机制加速”。
-O2在 RTL 层激活的关键开关:-ftree-loop-optimize:循环优化总开关,但注意——它本身不执行具体变换,只是打开后续-ftree-loop-*系列的门;-falign-functions=16:函数起始地址对齐到 16 字节边界,避免跨 cache line 取指(x86_64 下 L1i cache line 为 64 字节,16 字节对齐可确保 90% 函数不跨线);-fcrossjumping:跨跳转点代码合并,将if (x) { a(); } else { a(); }中的a()调用提至公共路径。
注意:
-falign-functions在 ARM64 上默认值是 32,而非 x86_64 的 16。这是因为 ARM64 的指令编码更紧凑(固定 4 字节),32 字节对齐能更好匹配其预取单元(prefetcher)的 stride。若你在 ARM64 项目中强制设为 16,实测发现 L1i cache miss rate 反而上升 7%——编译器默认值背后,是芯片厂商提供的微架构手册数据。
2.3 TREE 层:高级语义的“激进重构”
TREE 层优化发生在 GIMPLE 之上,涉及函数内联、循环向量化、跨函数分析等高风险高回报操作。这也是-O2与-O3分水岭所在——-O3在此层默认启用更多“可能破坏程序员直觉”的变换。
-O3特有的 TREE 层开关:-ftree-vectorize:自动向量化核心开关。它会扫描循环,尝试将for(i=0;i<1024;i++) a[i] += b[i];转为 AVX2 的 256 位并行加法;-funroll-loops:循环展开。但 GCC 实现很聪明——它先估算展开后的代码膨胀率,若超过--param max-unroll-times=8(默认值),则自动降级为部分展开;-finline-functions:激进内联。它会内联所有被调用次数 ≥ 2 且函数体大小 ≤--param large-function-growth=1000(单位:GIMPLE 指令数)的函数。
警告:
-funroll-loops在含printf的循环中极其危险。某次我误将日志循环加上此选项,编译后二进制体积暴涨 400KB(因printf的复杂格式解析逻辑被完整复制 8 份),而运行时性能毫无提升——因为printf本身就是 I/O 瓶颈,CPU 指令级并行毫无意义。正确做法是:对纯计算循环用-funroll-loops,对含系统调用的循环用-fno-unroll-loops。
2.4 LTO 层:跨翻译单元的“上帝视角”优化
当启用-flto(Link Time Optimization)时,GCC 在编译阶段不生成最终机器码,而是保存 GIMPLE IR 到.o文件中。链接时,ld调用lto-wrapper,将所有.o的 IR 合并,再进行一次全程序分析。此时编译器能看到file1.c中helper_func()的全部实现,以及file2.c中对它的 17 次调用点——从而做出单文件编译无法企及的决策:
- 全局内联:即使
helper_func()声明为static,只要它在多个文件中定义(违反 ODR),LTO 也能识别并合并; - 无用代码消除(UDE):若
file3.c定义了一个从未被调用的debug_dump_all(),LTO 会彻底删除其代码和数据; - 跨函数常量传播:
file1.c中#define MAX_SIZE 1024,file2.c中int buf[MAX_SIZE],LTO 能将buf优化为栈上 4KB 静态分配,而非运行时malloc(1024)。
实测数据:某网络协议栈(12 个 C 文件)启用
-flto -O2后,最终二进制体积减少 12.3%,关键路径延迟降低 9.7%(perf record 显示cache-misses下降 22%)。但代价是链接时间从 1.2 秒增至 8.4 秒——LTO 不是银弹,它适合发布构建,绝不该用于日常开发编译。
3. 性能敏感场景的 6 个关键选项实战指南
纸上谈兵不如真刀真枪。以下是我过去三年在高频交易系统、实时音视频 SDK、车载 ADAS 控制器三个项目中,反复验证有效的 6 个选项配置方案。每个方案都附带可复现的 benchmark 方法、汇编对比片段、以及踩坑血泪史。
3.1 场景一:低延迟网络服务(要求 P99 延迟 < 50μs)
问题:某 UDP 报文解析服务在-O2下 P99 达 62μs,超出 SLA。perf annotate显示热点在__memcpy_avx512的分支预测失败(branch-misses > 15%)。
根因分析:GCC-O2默认启用-finline-functions-called-once,导致报文解析函数被内联进事件循环,而memcpy调用点因数据长度可变,CPU 分支预测器频繁失效。
解决方案:
gcc -O2 -fno-inline-functions-called-once \ -march=native -mtune=native \ -fno-tree-vectorize \ -fno-semantic-interposition \ -o parser parser.c-fno-inline-functions-called-once:阻止解析函数内联,保留清晰的函数边界,让 CPU 分支预测器专注预测主循环而非内联后的复杂跳转;-fno-semantic-interposition:告知链接器“本程序中所有符号均无外部重定义”,使memcpy调用直接绑定到libc的 PLT 条目,省去dlsym运行时查找(实测dlsym调用耗时 120ns,而 PLT 直接跳转仅 1ns);-fno-tree-vectorize:禁用向量化,因报文长度随机(64~1500 字节),AVX512 向量化需额外的长度判断和掩码操作,反而拖慢小包处理。
效果:P99 降至 43μs,branch-misses从 15.2% 降至 3.8%。汇编对比关键片段:
# -O2 默认(内联后) .L23: cmpq $1500, %rax # 比较长度 ja .L25 # 若超长,跳转到非向量化路径 vmovdqu64 (%rsi), %ymm0 # AVX512 加载 # ... 向量化处理 ... .L25: # fallback to scalar memcpy # 加 -fno-inline-functions-called-once 后 call parse_packet@PLT # 清晰调用,分支预测器只学这一条3.2 场景二:嵌入式 MCU 固件(Flash ≤ 256KB,RAM ≤ 64KB)
问题:某 STM32H7 系统启用-O2后,.text段达 268KB,超出 Flash 限制。arm-none-eabi-size -A显示lib_a-memcpy.o占 42KB。
根因分析:-O2启用的-ffunction-sections让链接器能丢弃未用函数,但newlib的memcpy是强符号,且 GCC 为 ARM 生成了包含 8 种对齐版本的巨无霸实现。
解决方案:
arm-none-eabi-gcc -Os -mcpu=cortex-m7 -mfpu=fpv5-d16 -mfloat-abi=hard \ -fno-builtin-memcpy \ -fshort-enums \ -fdata-sections -ffunction-sections \ -Wl,--gc-sections \ -o firmware.elf firmware.c-fno-builtin-memcpy:禁用 GCC 内置memcpy,强制链接newlib的精简版(或你自定义的memcpy);-fshort-enums:枚举类型用最小整型存储(如enum {A,B}占 1 字节而非默认 4 字节),对状态机代码体积影响显著;-fdata-sections -ffunction-sections -Wl,--gc-sections:为每个函数/数据生成独立 section,链接时自动裁剪未引用部分。
效果:.text从 268KB 降至 193KB,节省 75KB。关键技巧:在startup_stm32h7xx.s中,将memcpy替换为汇编手写版(仅处理 4 字节对齐场景),再配合-fno-builtin-memcpy,体积进一步压缩 11KB。
3.3 场景三:科学计算密集型(矩阵乘、FFT)
问题:OpenBLAS 的 SGEMM 内核在-O3 -march=native下,比 Intel MKL 慢 35%。objdump -d发现 GCC 生成了大量vmovaps(寄存器间拷贝),而非预期的vaddps直接计算。
根因分析:-O3默认启用-ftree-slp-vectorize(Superword-Level Parallelism),它试图将标量指令打包成向量,但对 BLAS 这类已高度手写汇编的库,SLP 会破坏原有寄存器分配策略,引入冗余移动。
解决方案:
gcc -O3 -march=native -mtune=native \ -fno-tree-slp-vectorize \ -ftree-vectorize \ -mprefer-avx128 \ -funroll-loops \ -o sgemm_kernel sgemm.c-fno-tree-slp-vectorize:关闭 SLP,保留手写汇编的寄存器布局;-ftree-vectorize:仍开启循环级向量化(对for(i=0;i<n;i++)这类外层循环有效);-mprefer-avx128:在支持 AVX512 的 CPU 上,优先用 128 位指令(AVX),而非 512 位(AVX512)。实测 AVX512 在某些矩阵尺寸下因频率降频(Turbo Boost 限制),反而比 AVX 慢 12%。
效果:SGEMM 性能提升 28%,接近 MKL 的 92%。汇编验证:vaddps %ymm0,%ymm1,%ymm2(AVX256)替代了原vmovaps %ymm0,%ymm3; vaddps %ymm3,%ymm1,%ymm2的冗余移动。
3.4 场景四:调试友好的 Release 构建(DevOps CI/CD)
问题:CI 流水线用-O2构建的二进制,gdb调试时变量显示为<optimized out>,无法定位线上偶发崩溃。
根因分析:-O2启用的-fomit-frame-pointer删除了帧指针,使gdb无法可靠回溯调用栈;同时-finline-functions导致函数边界消失。
解决方案:
gcc -O2 -g -fno-omit-frame-pointer \ -fno-inline-functions \ -fno-tree-dce \ -o service service.c-fno-omit-frame-pointer:强制保留%rbp作为帧指针,gdb backtrace准确率从 63% 提升至 99%;-fno-inline-functions:禁用所有内联,保持原始函数结构;-fno-tree-dce:禁用死代码消除,确保调试信息中的变量声明与实际代码存在对应关系。
经验:此配置下二进制体积仅比纯
-O2增加 3.2%,但调试效率提升十倍。某次线上SIGSEGV,运维同事用coredump+ 此构建的二进制,3 分钟内定位到空指针解引用,而之前需 2 小时。
3.5 场景五:容器化微服务(启动时间敏感)
问题:某 Go 语言服务通过 CGO 调用 C 加密库,docker run启动耗时 1.8 秒,其中 0.9 秒在dlopen加载libcrypto.so。
根因分析:-O2启用的-fPIE(Position Independent Executable)使共享库需在加载时重定位,而-fno-semantic-interposition可跳过部分重定位步骤。
解决方案:
gcc -O2 -fPIE -fno-semantic-interposition \ -shared -o libcrypto.so crypto.c-fno-semantic-interposition:告诉动态链接器“本库中所有符号不会被 LD_PRELOAD 覆盖”,省去符号解析时的 interposition 检查(实测dlopen时间减少 310ms)。
效果:容器启动时间从 1.8s 降至 1.49s。注意:此选项仅适用于你完全控制部署环境的场景(如私有云),若服务需在客户环境运行,禁用此选项以防兼容性问题。
3.6 场景六:实时音视频编解码(确定性延迟)
问题:WebRTC 的 VP8 解码器在-O3下出现音频卡顿,perf sched latency显示调度延迟峰值达 8ms(要求 < 2ms)。
根因分析:-O3的-funroll-loops导致解码循环体过大,L1i cache 无法容纳,引发频繁的 instruction cache miss,CPU 停顿等待取指。
解决方案:
gcc -O2 -march=haswell \ -fno-unroll-loops \ -falign-loops=16 \ -foptimize-sibling-calls \ -o libvpx.so vpx_decoder.c-fno-unroll-loops:禁用循环展开,保持循环体紧凑;-falign-loops=16:循环起始地址对齐到 16 字节,确保 95% 的循环体落在同一 L1i cache line;-foptimize-sibling-calls:优化尾递归调用(如decode_block()末尾调用decode_next_block()),将其转为jmp而非call/ret,减少栈操作。
效果:最大调度延迟从 8.2ms 降至 1.7ms,卡顿消失。perf stat -e instructions,cycles,L1-icache-misses显示L1-icache-misses下降 64%。
4. 避坑指南:那些 GCC 文档不会告诉你的真相
GCC 手册写的是“应该发生什么”,而真实世界运行的是“实际发生什么”。以下是我在生产环境踩过的 5 个深坑,每个都附带复现方法、根本原因、以及绕过方案。
4.1 坑一:-O3下volatile变量被意外优化
复现场景:
// driver.c volatile uint32_t *reg = (uint32_t*)0x40000000; void wait_for_ready() { while (*reg & 0x1) {} // 等待寄存器 bit0 清零 }用-O3编译后,while循环被优化为无限jmp,因 GCC 认为*reg的值不会改变(未识别volatile的硬件语义)。
根因:-O3启用的-ftree-loop-if-convert-stores会将循环中的 store 操作转换为条件 store,但对volatile访问的依赖分析有缺陷。GCC 7.5+ 已修复,但旧版(如 CentOS 7 默认 GCC 4.8.5)仍存在。
绕过方案:
- 强制添加内存屏障:
while (*reg & 0x1) __asm__ volatile("" ::: "memory");; - 或降级为
-O2(-O2不启用此问题开关); - 最佳实践:对硬件寄存器访问,始终用
__io宏封装,并在 Makefile 中为驱动模块单独指定-O2。
4.2 坑二:-flto与weak符号的链接冲突
复现场景:
// logger.c __attribute__((weak)) void log_debug(const char *msg) { printf("DEBUG: %s\n", msg); } // main.c void log_debug(const char *msg); // 未定义,期望用 weak 版本 int main() { log_debug("start"); }启用-flto后,链接时报错undefined reference to 'log_debug'。
根因:LTO 在全局分析时,将weak函数视为“可能被覆盖”,若main.c中有log_debug的声明但无定义,LTO 会认为该符号必须由外部提供,从而忽略weak定义。
绕过方案:
- 在
logger.c中,将weak改为__attribute__((weak, alias("log_debug_default"))),并提供log_debug_default实现; - 或禁用 LTO 的跨文件分析:
-flto=jobserver(仅限 GNU make); - 更稳妥:避免在 LTO 项目中使用
weak,改用函数指针注册表。
4.3 坑三:-march=native在 Docker 构建中失效
复现场景:CI 流水线在 AMD EPYC 服务器上构建镜像,Dockerfile中写RUN gcc -O2 -march=native -c test.c,但生成的二进制在 Intel Xeon 客户端运行时报Illegal instruction。
根因:-march=native读取的是宿主机 CPUID,而 Docker 容器共享宿主机内核,gcc编译时检测到 EPYC 的avx512f指令集,生成了 AVX512 指令。但客户端 Xeon 可能只支持 AVX2。
绕过方案:
- 构建时显式指定目标:
-march=x86-64-v3(涵盖 AVX2, BMI2 等,Intel/AMD 主流 CPU 均支持); - 或用
docker build --platform linux/amd64/v3(需 Docker 20.10+); - 绝对禁止:在 CI/CD 中使用
-march=native,它只适用于本地开发机。
4.4 坑四:-fPIC与全局构造函数的初始化顺序
复现场景:
// plugin.c __attribute__((constructor)) void init_plugin() { global_ptr = malloc(1024); // 初始化全局指针 } // main.c extern void* global_ptr; int main() { printf("%p\n", global_ptr); } // 输出 0x0用-fPIC编译plugin.so后,global_ptr为 NULL。
根因:-fPIC生成的位置无关代码,其全局构造函数(.init_array)执行时机晚于主程序的main(),因动态链接器需先解析所有符号再执行构造函数。
绕过方案:
- 改用
__attribute__((constructor(101))),指定高优先级(101 > 默认 100); - 或在
main()中显式调用init_plugin(); - 根本解决:避免在 shared library 中用 constructor 初始化跨模块全局状态。
4.5 坑五:-O2下assert()被完全删除
复现场景:
#include <assert.h> int main() { int *p = NULL; assert(p != NULL); // 期望崩溃,但程序静默退出 *p = 1; }-O2编译后,assert消失,程序 segfault。
根因:-O2启用-DNDEBUG宏(GCC 默认行为),使assert.h中的assert展开为空宏。这不是 bug,而是 POSIX 标准要求——NDEBUG下assert必须无副作用。
绕过方案:
- 开发构建用
-O0 -UNDEBUG; - 生产构建用
-O2 -DNDEBUG,但关键检查改用if (!p) abort();; - 最佳实践:用
static_assert(编译期)替代assert(运行期)做可验证的约束。
5. 终极调试工具链:从汇编到 perf 的全链路验证
选对选项只是开始,验证它是否生效才是关键。以下是我每天都在用的 4 层验证法,覆盖从编译期到运行时的全链路。
5.1 第一层:编译期确认(-Q --help=optimizers)
这是最基础的验证,确认 GCC 是否按你预期启用了选项:
# 查看 -O2 启用了哪些优化 gcc -O2 -Q --help=optimizers | grep enabled # 查看特定选项状态(如 -ftree-vectorize) gcc -O2 -Q --help=optimizers | grep tree-vectorize # 输出: -ftree-vectorize [enabled] # 查看所有优化参数的当前值 gcc -O2 -Q --help=params | grep max-unroll # 输出: max-unroll-times 8提示:
-Q输出中[enabled]表示默认启用,[disabled]表示默认禁用,[enabled by -O2]表示被-O2激活。务必逐行核对,不要凭记忆。
5.2 第二层:汇编级验证(-S与objdump)
生成汇编并人工审查,是最可靠的验证方式:
# 生成汇编(不经过汇编器,纯文本) gcc -O2 -S -o test.s test.c # 生成带源码注释的汇编(最实用) gcc -O2 -g -Wa,-adhln -c test.c > test.lst # 反汇编已编译的二进制,查看实际指令 objdump -d -M intel --no-show-raw-insn test | less关键审查点:
- 循环是否被展开?搜索
jmp后是否紧跟相同指令块; memcpy是否被内联?搜索vmovdqu等向量指令,而非call memcpy@PLT;- 函数是否被内联?搜索
call指令,若某函数调用点消失,则已被内联。
5.3 第三层:运行时行为验证(perf与valgrind)
用性能工具观测真实行为:
# 记录 CPU 事件(cache miss, branch mispred) perf record -e cycles,instructions,cache-misses,branch-misses \ -g ./test_program # 生成火焰图(需 FlameGraph 工具) perf script | ./FlameGraph/stackcollapse-perf.pl | ./FlameGraph/flamegraph.pl > perf.svg # 检查内存访问模式(是否触发 false sharing) valgrind --tool=cachegrind --cachegrind-out-file=cg.out ./test_program cg_annotate cg.out | head -20解读技巧:
- 若
cache-misses高,检查是否因-funroll-loops导致代码膨胀,超出 L1i cache; - 若
branch-misses高,检查是否因内联破坏了分支预测器的学习能力; cachegrind的DwMisses(数据写缺失)若异常高,可能是volatile访问未对齐,触发 cache line 拆分。
5.4 第四层:构建产物验证(size,readelf,nm)
分析二进制文件结构,确认优化效果:
# 查看各段大小(重点关注 .text) arm-none-eabi-size -A firmware.elf # 查看符号表,确认未用函数是否被裁剪 arm-none-eabi-nm -C -u firmware.elf | grep " U " # 显示未定义符号 # 查看动态符号(对 shared library) readelf -d libcrypto.so | grep NEEDED关键指标:
.text段变化率:(-O2 -Os) / -O2应 < 5%(否则-Os可能过度牺牲性能);nm输出中U符号数量:启用-fdata-sections -ffunction-sections -Wl,--gc-sections后,应比未启用时减少 30%+;readelf -d中NEEDED库数量:-fno-semantic-interposition不会改变此数量,但会减少PLT条目数。
6. 我的个人经验:如何建立属于你的优化决策树
写了十年 C/C++,我总结出一套不依赖文档、不迷信-O3的实战决策流程。它不是 checklist,而是一个动态演化的思维框架:
6.1 第一步:定义你的“不可妥协红线”
在敲下第一个编译命令前,先写下三条硬性约束:
- 延迟红线:P99 必须 < 100μs,还是平均延迟 < 10ms?前者关注 worst-case,后者关注 throughput;
- 体积红线:Flash 剩余空间 < 16KB,还是 RAM 使用率 < 70%?前者决定能否塞进 MCU,后者决定是否触发 OOM killer;
- 调试红线:必须支持
gdb实时调试,还是只需coredump分析?前者要求保留帧指针,后者可激进优化。
我的教训:曾为追求极致性能,在车载 ECU 中启用
-O3 -funroll-loops,结果因体积超标导致 OTA 升级包超限,被迫回滚。现在我的 Makefile 里第一行就是:# WARNING: DO NOT EXCEED $(FLASH_LIMIT) KB — CHECKED BY CI
红线不是限制,而是让你在优化迷宫中不迷路的北极星。
6.2 第二步:用perf建立基线,而非用time
time ./program只给你一个数字,而perf stat -r 5 ./program给你 5 次运行的统计分布:
# 关键字段解读 Performance counter stats for './test' (5 runs): 12,345,678,901 cycles # CPU 周期总数(越少越好) 8,765,432,109 instructions # 执行指令数(IPC = instructions/cycles,理想 > 1.0) 234,567,890 cache-misses # cache miss 次数(越少越好) 12,345,678 branch-misses # 分支预测失败(越少越好) 1,234,567 task-clock (m