第 9 讲 · 编译期优化:让编译器和反汇编告诉你真相

-fopt-info 定位向量化失败的原因;用 objdump 验证编译选项是否生效;理解 aarch64 专属选项的依赖关系树。

编译期是地基,不是装修

第 1 讲看到,-O2 会自动向量化简单的循环,但复杂代码经常失败。常见原因:循环里有函数调用、有复杂控制流、指针别名无法证明(编译器不敢保证数组不重叠)、有跨迭代的数据依赖。你写的是"应该能向量化"的循环,编译器却没做------而你毫不知情。

让编译器开口:-fopt-info

GCC 提供了 -fopt-info 系列选项,输出优化决策的详细信息。三种语法形式:

text 复制代码
-fopt-info
-fopt-info-options
-fopt-info-options=filename

消息类别(决定你想看什么):

类别 含义
optimized 成功应用的优化
missed 错过的优化------诊断向量化失败用这个
note 详细注释信息
all 以上全部

优化组 (决定关注哪个领域):ipa(过程间)、loop(循环)、inline(内联)、vec(向量化)、optall(全部)。

两者可以组合,且顺序无关-fopt-info-vec-missed-fopt-info-missed-vec 等价。

诊断向量化失败

写一个"看起来该向量化但编译器没做"的例子:

c 复制代码
/*
 * vec_fail.c ------ 一个难以向量化的循环,用于演示诊断方法
 *
 * 编译:gcc -O3 -march=armv8.1-a \
 *         -fopt-info-vec-optimized=vec.opt \
 *         -fopt-info-vec-missed=vec.miss \
 *         -c vec_fail.c -o vec_fail.o
 */
#include <stddef.h>

/* 这个循环会被向量化 */
void add_simple(float *restrict c, const float *restrict a,
                const float *restrict b, int n)
{
    for (int i = 0; i < n; i++) c[i] = a[i] + b[i];
}

/* 这个循环不会被向量化 ------ 因为循环体内有函数调用 */
static float transform(float x) { return x * 2.0f + 1.0f; }

void add_with_call(float *restrict c, const float *restrict a, int n)
{
    for (int i = 0; i < n; i++) c[i] = transform(a[i]);
}

/* 这个循环也可能不被向量化 ------ 存在跨迭代依赖 */
void scan_dependency(float *restrict a, int n)
{
    for (int i = 1; i < n; i++) a[i] = a[i] + a[i - 1];
}

编译并查看报告:

bash 复制代码
gcc -O3 -march=armv8.1-a \
    -fopt-info-vec-optimized=vec.opt \
    -fopt-info-vec-missed=vec.miss \
    -c vec_fail.c -o vec_fail.o

echo "=== 成功向量化 ==="
cat vec.opt
echo "=== 未能向量化及原因 ==="
cat vec.miss

vec.miss 里会给出形如 couldn't vectorize loop ... not vectorized: function call in loopnot vectorized: complicated access pattern 的说明。这些提示直接指向了修改方向 ------把函数内联、改用 restrict 消除别名歧义、重写循环结构。

⚠️ 一个使用上的限制-fopt-info 只支持 GCC 内置的优化组(vec/loop/ipa/inline 等),没有按鲲鹏专有选项名过滤的能力 (不存在 -fopt-info-crc 这类)。所以对于 -floop-crc-fsplit-ldp-stp 这些 openEuler 扩展选项,要验证它们是否生效得靠反汇编

用反汇编验证选项真的生效

写一个包含 CRC 计算的函数------aarch64 有专门的 CRC 指令,如果 -floop-crc 生效,汇编里会出现 crc32* 系列指令:

c 复制代码
/*
 * crc_demo.c ------ 用于验证 -floop-crc 是否生效
 *
 * 编译(优化版):
 *   gcc -O3 -march=armv8.1-a -floop-crc -o crc_opt crc_demo.c
 * 编译(基线版):
 *   gcc -O3 -march=armv8.1-a -o crc_base crc_demo.c
 *
 * 验证:
 *   objdump -d crc_opt  | grep -E "crc32[bhwx]"   # 应该有输出
 *   objdump -d crc_base | grep -E "crc32[bhwx]"   # 可能没有
 */
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>

/* 逐字节 CRC32 计算 ------ 软件实现,编译器可以识别并替换为硬件指令 */
uint32_t crc32_bytewise(const uint8_t *data, size_t len)
{
    uint32_t crc = 0xFFFFFFFFu;

    for (size_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (int j = 0; j < 8; j++) {
            /* 逐位处理:这是 CRC 的标准软件实现写法 */
            crc = (crc >> 1) ^ (0xEDB88320u & (uint32_t)(-(int32_t)(crc & 1)));
        }
    }

    return ~crc;
}

int main(void)
{
    uint8_t buf[4096];
    for (size_t i = 0; i < sizeof(buf); i++) buf[i] = (uint8_t)i;

    printf("CRC32 = 0x%08X\n", crc32_bytewise(buf, sizeof(buf)));
    return 0;
}

编译两份并对比:

bash 复制代码
# 优化版:带 -floop-crc
gcc -O3 -march=armv8.1-a -floop-crc -o crc_opt crc_demo.c

# 基线版:不带
gcc -O3 -march=armv8.1-a -o crc_base crc_demo.c

# 对比 CRC 指令数量
echo -n "优化版 crc32 指令数: "; objdump -d crc_opt  | grep -cE "crc32[bhwx]"
echo -n "基线版 crc32 指令数: "; objdump -d crc_base | grep -cE "crc32[bhwx]"

# 对比总指令数
echo -n "优化版总指令数: "; objdump -d crc_opt  | wc -l
echo -n "基线版总指令数: "; objdump -d crc_base | wc -l

一条 crc32 硬件指令替代了 8 次循环迭代------这就是选项生效的直接证据。

⚠️ 待实机确认:-floop-crc 要求 -O3 -march=armv8.1-a,这是官方文档明确的前提。但该选项是否在你使用的 GCC 版本上存在 需要确认------它属于 openEuler 的 GCC 增强,主线 GCC 未必包含。用 gcc --version 确认版本,如果选项不存在,编译会直接报 unrecognized command-line option

aarch64 专属选项的依赖树

关键认知:这些选项不是一堆独立的开关,而是一棵有依赖关系的树。

前提一:优化等级够高。 很多选项明确要求 -O3 及以上。原因是它们依赖的中间表示分析(循环级数据流分析、跨函数别名分析)只在激进优化等级下才完整执行。-O2 下这些分析压根没跑,选项加上了也无事可做。

前提二:目标架构声明正确。 有些选项要求 -march 含特定扩展。比如 -floop-crc 要求 -march=armv8.1-a------因为**armv8.1-a 默认就包含 +crc**(官方文档明确:armv8.1-a 包含 armv8-a+crc+lse+rdma)。

前提三:链接时可见性。 跨函数优化(-ficp-fipa-struct-reorg 等)还要求 LTO 打开,甚至要求 -flto-partition=one

理解了这三条,就不会犯"把网上看到的选项全加上"的错误------依赖不满足,选项被静默忽略,你还以为优化生效了。

按依赖分组的选项清单

只需 -O3-O2(最容易用上的一批)

选项 作用 前提
-floop-crc CRC 循环 → 硬件指令 -O3 -march=armv8.1-a
-fcrypto-accel-aes AES 序列 → 硬件指令 -O3
-fsplit-ldp-stp 拆分性能较差的成对加载/存储 -O1 及以上
-fconvert-minmax min/max 联合优化 -O3
-mcmlt-arith 生成 cmlt 指令 -O3
-ftree-slp-transpose-vectorize SLP 转置向量化 -O3
-fllc-allocate 热点数据预取到 LLC -O2
--param=vect-alias-flexible-segment-len=1 向量化增强 -O3

必须配合 -flto(跨函数优化)

选项 作用 前提
-ficp -ficp-speculatively 间接调用提升为直接调用 -O2 -flto -flto-partition=one
-fipa-prefetch -fipa-ic 间接访存插入预取 -O3 -flto
-fipa-struct-reorg 结构体成员重排 -O3 -flto -flto-partition=one
-fipa-reorder-fields 成员按大小排序 同上
-fipa-struct-sfc 结构体静态压缩 同上,且需 -fipa-reorder-fields-fipa-struct-reorg>=2

SVE 路径(需要 CPU 支持):

选项 前提
-floop-sve-mode-opt -O3-marchsve
-ffind-with-sve std::find 的 SVE 优化

⚠️ 关于 SVE 的重要提醒 :鲲鹏 920 不支持 SVE 。KSYS 文档明确说明 SVE/SME 数据采集仅部分鲲鹏 920 新型号支持。所以面向标准 920 的优化不要用 SVE 路径 ------生成的指令无法执行。判断依据:用 lscpu 看 CPU 型号,或查 GCC 的 -mcpu 支持列表。

实用的完整编译命令

基础增强组合(单文件,无 LTO):

bash 复制代码
gcc -O3 -march=armv8.1-a \
    -floop-crc \
    -fsplit-ldp-stp --param=param-ldp-dependency-search-range=16 \
    -fconvert-minmax \
    -mcmlt-arith \
    --param=vect-alias-flexible-segment-len=1 \
    -fcrypto-accel-aes \
    -ftree-slp-transpose-vectorize \
    -fllc-allocate \
    -o app_opt app.c -lm

LTO 组合(多文件,IPA 类选项必须同一条命令给出全部源文件):

bash 复制代码
gcc -O3 -march=armv8.1-a -flto -flto-partition=one \
    -ficp -ficp-speculatively \
    -fipa-struct-reorg \
    -fipa-reorder-fields \
    -fipa-prefetch -fipa-ic \
    -o app_opt app.c helper.c -lm

关键约束 :带 -flto -flto-partition=one 的选项,必须在同一条链接命令里给出全部源文件 ,否则 LTO 分区被打断,优化失效。这跟"先 -c 编译成 .o 再链接"的常规做法冲突------用这些选项时要改成一次性编译。

关于 -mcpu 与 -march 的选择

GCC 的 AArch64 文档确认:-mcpu=native 是合法的native 同时是 -march-mcpu 的允许值,会让编译器识别宿主架构)。网上"aarch64 不支持 -mcpu=native"的说法不准确。

但工程上仍推荐显式写目标 CPU

  1. -mcpu=native 把产物绑死在构建机上,跨机分发会因非法指令崩溃
  2. 若宿主不可识别,-march=native静默降级为默认值------性能没提升却查不出原因

鲲鹏 920 对应的 GCC CPU 名是 tsv110 (在 GCC 的 -mcpu 允许值列表中):

bash 复制代码
# 面向鲲鹏 920 调优
gcc -O2 -mcpu=tsv110 -o app app.c

# 跨机安全的写法:明确架构基线 + 需要的扩展
gcc -O2 -march=armv8-a+crc -o app app.c

# armv8.1-a 默认含 crc 和 lse
gcc -O2 -march=armv8.1-a -o app app.c

一个可复用的验证脚本

把上面的方法固化成脚本,每次改编译选项都能快速验证:

bash 复制代码
#!/bin/bash
# verify_opts.sh ------ 对比两个编译版本的指令差异
# 用法:./verify_opts.sh <基线可执行文件> <优化可执行文件>

BASE=$1
OPT=$2

if [ -z "$BASE" ] || [ -z "$OPT" ]; then
    echo "用法: $0 <基线可执行文件> <优化可执行文件>"
    exit 1
fi

echo "=== 指令总数 ==="
printf "基线: %s\n" "$(objdump -d "$BASE" | wc -l)"
printf "优化: %s\n" "$(objdump -d "$OPT"  | wc -l)"

echo ""
echo "=== 关键指令统计 ==="
for pattern in "crc32[bhwx]:CRC" "aese|aesd|aesmc:AES" \
               "\bfmla\b:FMLA(融合乘加)" "\bldp\b|\bstp\b:LDP/STP(成对访存)"; do
    pat="${pattern%%:*}"
    name="${pattern##*:}"
    b=$(objdump -d "$BASE" | grep -cE "$pat" || true)
    o=$(objdump -d "$OPT"  | grep -cE "$pat" || true)
    printf "%-22s 基线 %5s  优化 %5s\n" "$name" "$b" "$o"
done

echo ""
echo "=== 向量指令数量(NEON/ASIMD)==="
printf "基线: %s\n" "$(objdump -d "$BASE" | grep -cE '\b(fadd|fmul|fmla)\s+v[0-9]' || true)"
printf "优化: %s\n" "$(objdump -d "$OPT"  | grep -cE '\b(fadd|fmul|fmla)\s+v[0-9]' || true)"

用法:

bash 复制代码
chmod +x verify_opts.sh
./verify_opts.sh crc_base crc_opt

动手练习

  1. -fopt-info-vec-missed 分析第 2 讲的 bench_add.c,找出没被向量化的循环及原因。
  2. add_with_call 加上 static inline 重新编译,验证"函数调用阻碍向量化"的解法是否生效。
  3. verify_opts.sh 对比 -O2-O3 -march=armv8.1-a 两个版本的指令统计,找出变化最大的指标。

进阶与拓展

  • -mcpu=tsv110 的调优面-mcpu 等于"架构扩展 + 调度调优"的组合-------march 只定指令集基线,-mtune 单独控制指令调度与成本模型。想保住兼容基线又吃鲲鹏 920 的调度调优,可以拆开写:-march=armv8.2-a -mtune=tsv110(产物不含 920 独有指令,但调度按 920 优化)。
  • -O3 的风险 :相对 -O2-O3 多开激进变换(更深循环展开、更宽向量化),代码体积增长可能加重指令缓存压力,并非全场景正收益。上 -O3 前先用本文的 verify_opts.sh 看指令数变化,再以真实负载 A/B 实测,不要默认"等级越高越好"。
  • LTO 的组合用法与代价-flto-ficp-fipa-struct-reorg 等 IPA 类选项配套使用(见上文依赖表);代价是构建时间和链接期内存显著上升,-flto-partition=one 尤其明显,大工程建议先在单个模块试点。
  • 探测选项是否存在 :openEuler 扩展选项依赖特定 GCC 版本,拿不准就编译一个只含空 main 的文件试一下,报 unrecognized command-line option 即不存在;gcc -Q --help=target 可查当前编译器的目标选项与默认值。

参考来源

相关推荐
无限压榨切图仔1 小时前
加了“去 AI 味”Skill,水稿还是水:AI 写作流程缺的不是润色
人工智能·程序员
天远数科1 小时前
零信任架构实战:基于天远车型识别精准构建自动化二手车评估网关
人工智能·ai·工具分享
用户1917291270831 小时前
多 Agent 并行不打架:worktree 隔离与反馈回流落地(附脚本)
人工智能
亦暖筑序1 小时前
AgentScope Java 实战:Agent 的状态存在哪、怎么恢复、怎么隔离?
人工智能·后端·agent
jsl_jsl_jsl1 小时前
《Bun 后端怎么变桌面软件:Tauri 2 三进程架构与崩溃自愈》
人工智能
lucas_AI1 小时前
第 10 讲 · PGO 实战:让程序的真实运行数据指导编译
人工智能
lucas_AI1 小时前
第 8 讲 · oeAware 与中断绑核:把手工调优自动化
人工智能
hyunbar7771 小时前
LangChain 实战:上下文工程长期记忆管理
人工智能
lucas_AI1 小时前
第 11 讲 · KAE 硬件加速:不改一行业务代码的性能提升
人工智能