用
-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 loop 或 not 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 且 -march 含 sve |
-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:
-mcpu=native把产物绑死在构建机上,跨机分发会因非法指令崩溃- 若宿主不可识别,
-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
动手练习
- 用
-fopt-info-vec-missed分析第 2 讲的bench_add.c,找出没被向量化的循环及原因。 - 给
add_with_call加上static inline重新编译,验证"函数调用阻碍向量化"的解法是否生效。 - 用
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可查当前编译器的目标选项与默认值。
参考来源
- GCC · AArch64 Options ---
-march/-mcpu语法与默认特性(armv8.1-a含+crc)、tsv110CPU 名、native的合法性与行为 - GCC · Developer Options(-fopt-info) --- 消息类别、优化组、输出文件规则与顺序无关性
- openEuler · GCC 基础性能优化用户指南 --- aarch64 专属选项全表与逐项使能条件、
-fllc-allocate的--param默认值