周末你走进一家餐厅,点了一份番茄炒蛋。后厨里站着四位师傅,拿的是同一张菜谱,端上桌的速度却完全不同:学徒照着步骤一步步来,慢但每一步都透明;熟手会提前把锅烧热、把葱切好;主厨让切菜、打蛋、热锅同时进行,稳定又高效;而那位疯狂总厨为了上菜速度,同时开三个灶,还把“先放糖再放盐”擅自改成“盐糖一起下”——大多数时候更快,偶尔味道就不对。这就是编译器优化级别的本质:同一份源代码,交给不同“火候”的编译器,会生成性能、体积甚至正确性都不同的可执行文件。本文把 -O0、-O1、-O2、-O3 这四档讲透,帮你决定生产环境该请哪位师傅。

岗位关联

学会判断在哪一级优化上做性能调优,能让你的生产构建既快又稳,是系统程序员的基本功。

目录
  1. 可视化演示
  2. 优化等级总览
  3. 编译优化原理
  4. O2核心优化
  5. O3激进手法
  6. 浮点精度影响
  7. 风险与边界
  8. 场景选型
  9. 性能验证
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示-O0到-O3优化级别性能对比、函数内联展开与循环优化过程、死代码消除与常量传播原理。切换模式探索编译器优化核心机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

优化级别O0到O3性能对比 O0无优化快速编译,O1基本优化,O2平衡性能与安全,O3激进优化含向量化 O3可能带来浮点精度变化和代码膨胀风险,生产环境推荐O2 O3 = O2 + vectorize + loop_unroll + fast_math? 选择正确优化级别是性能调优的关键

优化等级总览

厨房里的四位师傅,其实就是编译器面对同一份源代码时的四种工作模式。-O0 是学徒:老老实实照菜谱做,每条指令都保持源码顺序,变量和断点都能对上,调试体验最好,但速度最差。-O1 是熟手:顺手做些局部调整,比如把两个相邻的取食材动作合并。-O2 是默契主厨:统筹整条流水线,在安全前提下尽量让各个环节并行、不浪费,这也是绝大多数发行版和项目的默认选择。-O3 则是疯狂总厨:为了上菜速度可以使用更激烈的手段,但偶尔会把菜做糊。

在编译器里,-O 后面跟的数字不是“一个开关”,而是一整套优化通行证(pass)的组合。-O0 基本不做优化,-O1 做局部优化,-O2 在 -O1 基础上加入更多全局优化,-O3 又在 -O2 之上追加自动向量化、循环展开等激进手法。以 GCC 为例,-O2 是社区验证过的“性价比之王”;-O3 则更像一个高风险高收益的选项。

所以选优化级别就像选师傅:如果还在反复尝菜,就用学徒档 -O0;如果正式营业,主厨档 -O2 是稳妥的主力;除非那道菜值得让疯狂总厨赌一把,再考虑 -O3。

−O0<−O1<−O2<−O3\displaystyle -\mathcal{O}0 < -\mathcal{O}1 < -\mathcal{O}2 < -\mathcal{O}3
这个不等式只表示编译器会开启越来越多的优化手段,并不保证数字越大,程序一定跑得越快。
  1. 优化档位记号-\mathcal{O} 是传给编译器的优化档位前缀,后面跟的数字代表激进程度。
    −O-\mathcal{O}
  2. 数字含义0/1/2/3 分别是“调试”“基础”“标准”“激进”四档。
    0, 1, 2, 30,\ 1,\ 2,\ 3
  3. 递增关系< 表示后一档通常包含前一档的优化,并追加更多变换。
    O2⊂O3\mathcal{O}2 \subset \mathcal{O}3

编译优化原理

疯狂总厨为什么敢同时开三个灶?因为他手里有一份提前拆好的“操作清单”,而不是盯着菜谱上的汉字发呆。编译器也一样,它不会直接在源码文本上修改,而是先把源码变成一棵语法树(AST),再转成一种更接近机器但又保留程序结构的中间表示(IR)。所有优化都在 IR 上完成。

有了 IR,编译器还要画出一张“后厨动线图”——控制流图,标出哪段代码在什么条件下执行。接下来靠数据流分析,判断某个变量的值会不会被用到、某段计算是不是白做。函数内联就像“把外包给别人的步骤直接写进当前流程”,省去来回传递的运输成本;循环变换则像“把一盘菜分三锅同时炒”,是循环优化的基础。

把“拆步骤”和“真正炒菜”分开,是后厨高效运转的秘诀;编译器把“优化”和“生成机器码”分开,也是不同语言能共享优化成果的原因。

源码 → AST → IR → 汇编码\displaystyle \text{源码}\ \rightarrow\ \text{AST}\ \rightarrow\ \text{IR}\ \rightarrow\ \text{汇编码}
优化主要发生在 IR 阶段,这也是为什么不同前端语言(C++、Rust)可以共享同一套后端优化。
  1. 源码解析编译器先读入源码,按语法规则生成语法树。
    源码→AST\text{源码} \rightarrow \text{AST}
  2. 生成中间表示语法树被翻译成与机器无关的 IR,后续优化都在这里进行。
    AST→IR\text{AST} \rightarrow \text{IR}
  3. 生成汇编码优化完的 IR 最终被翻译成目标平台的汇编指令。
    IR→汇编码\text{IR} \rightarrow \text{汇编码}

O2核心优化

-O2 主厨不会做太出格的事,但他会做四件基本功:把熟客常点的固定配菜提前准备好(常量折叠),把永远不会用的食材扔掉(死代码消除),把先炒蛋还是先热锅的顺序排到最优(指令调度),以及对着出锅前的最后几秒做微调(窥孔优化)。这些优化单个看都不起眼,合在一起常常能带来可观的提升。

常量折叠是最直观的例子。如果代码里写着 x = 3 + 5 * 2,编译器在编译期就算出结果是 13,运行时连加法和乘法都不用执行。死代码消除则像一个厨师发现“这道菜根本不放香菜”,于是把洗香菜那一步整个删掉。指令调度和窥孔优化更像做菜的“手速调整”:在不改变结果的前提下,让 CPU 流水线少打磕碰。

-O2 的核心哲学是“不改变程序语义,只去掉多余动作”。它像一位经验丰富但守规矩的主厨,绝大多数项目用它,求的就是这份既快又稳的安心。

x=3+5×2⟹x=13\displaystyle x = 3 + 5 \times 2 \quad\Longrightarrow\quad x = 13
常量折叠把能在编译期算完的东西算完,把计算量留给真正必要的代码。
  1. 识别常量子表达式编译器看到 3 + 5 \times 2,知道两边的操作数都是常量。
    3+5×23 + 5 \times 2
  2. 按运算优先级求值先算 5 \times 2 = 10,再做加法,得到 13。
    5×2=105 \times 2 = 10
  3. 替换结果用 13 替换原公式,运行时不重复计算。
    x=13x = 13

O3激进手法

-O3 这位疯狂总厨开始动真格了。他会把一份菜分成四份同时下锅——这就是自动向量化:用 CPU 的 SIMD 指令一次处理多个数据。他还会把循环“摊开”,原来一次迭代做一件事,现在一次迭代做四件事,减少循环跳转的浪费。为了让函数调用更少,他甚至会把小函数直接复制到调用点,也就是激进内联。

自动向量化的收益可以用一个简单的速度比描述。假设标量版本处理一个数组元素要花 T_scalar 的时间,向量化版本一次处理四个元素,那么理论加速比接近四倍——前提是数据没有前后依赖,内存访问也没有互相覆盖。这就需要编译器做内存别名判断,确认两个指针不会指向同一块内存。如果判断失败,编译器只能保守地退回标量代码。

-O3 就像是把六个灶同时开满的总厨:如果菜量足够大,上菜速度确实惊人;但如果动线设计不好,几个灶反而互相阻碍。后面我们会看到,这种“阻碍”就是代码膨胀和缓存压力。

S=TscalarTvector\displaystyle S = \frac{T_{\text{scalar}}}{T_{\text{vector}}}
向量化是 -O3 最诱人的收益来源,但它的前提是对循环和内存访问有足够强的分析能力。
  1. 标量运行时间T_{\text{scalar}} 是每次只处理一个元素的版本耗时。
    TscalarT_{\text{scalar}}
  2. 向量运行时间T_{\text{vector}} 是一次处理多个元素的 SIMD 版本耗时。
    TvectorT_{\text{vector}}
  3. 加速比两者相除得到加速比 S,S > 1 说明向量化赚到了。
    S=Tscalar/TvectorS = T_{\text{scalar}} / T_{\text{vector}}

浮点精度影响

疯狂总厨最危险的一步,是擅自把“先放糖再放盐”改成“盐糖一起下”。对整数运算来说,顺序怎么换结果都一样;但浮点数不是这样。计算机里的浮点数是有限精度的,比如 0.1 + 0.2 在二进制里本身就是近似值,运算顺序一变,舍入误差就可能被放大。

默认情况下,-O2 会尊重 IEEE 754 浮点标准,尽量保持运算顺序,避免改变结果。但到了 -O3,尤其是配合 -ffast-math 这类选项时,编译器会认为“可以重新结合”浮点加法,甚至把除以一个数改成乘它的倒数。这些变换在数学上漂亮,在浮点世界里却可能让结果差出几个数量级。

FMA 融合是另一个常见例子:a * b + c 本来要两次舍入,融合成一次乘加指令后只舍入一次,结果更精确,但也和分开计算的结果不同。-O3 和 fast-math 给我们的是“更快的数学”,不一定是“原样的数学”。

(a+b)+c ≠ a+(b+c)\displaystyle (a + b) + c \ \neq\ a + (b + c)
浮点加法不满足结合律,是很多“换了优化级别结果就变”的根源。
  1. 两个加数a、b、c 是三个有限的浮点数,每一步都会舍入。
    a, b, ca,\ b,\ c
  2. 先加前两个(a + b) 先算出一个舍入后的中间结果,再加 c。
    (a+b)+c(a + b) + c
  3. 改变结合顺序a + (b + c) 先算 b + c,中间结果可能完全不同。
    a+(b+c)a + (b + c)

风险与边界

火开得越大,菜不一定上得越快。-O3 的循环展开会让可执行文件变大,代码太多就会挤爆 CPU 的指令缓存,反而比 -O2 更慢。编译时间也会明显增加,因为编译器要做更多分析和变换。更麻烦的是,这些效果在不同 CPU、不同编译器和不同库版本下可能完全不一样:在 Intel 上快了 10% 的代码,换到 ARM 上可能变慢 3%。

-O3 还建立在“代码是良定义”的假设上。如果程序本身有未定义行为,比如有符号整数溢出、违反严格别名规则、越界访问,那么 -O3 的激进分析会把这些隐藏问题彻底放大。编译器会认为某些情况“不可能发生”,然后按这个假设做优化,最后生成的行为可能完全不可预期。

所以疯狂总厨并不是每次都能赢。真正成熟的团队会把 -O3 当作“需要验收的特技”,而不是默认配置:先跑基准测试,确认它真的带来收益,再用测试用例确认精度和正确性没有崩塌。

T−O3 ≰ T−O2\displaystyle T_{-O3} \ \not\le \ T_{-O2}
这句话的意思是:-O3 的运行时间并不保证一定小于等于 -O2。
  1. -O3 运行时间T_{-O3} 表示用激进优化编译后的程序运行耗时。
    T−O3T_{-O3}
  2. 不等号\not\le 表示“不是总小于等于”,即可能更快,也可能更慢。
    ≰\not\le
  3. -O2 运行时间T_{-O2} 是默认优化档位下的运行耗时,作为对照基准。
    T−O2T_{-O2}

场景选型

请师傅要看场合。宴客大菜可以请疯狂总厨发挥,路边快餐需要主厨稳定出品,减脂餐则要求少油少盐,而菜品研发阶段最好让学徒一步一步来。编译优化级别也一样:没有永恒的“最高级”,只有在不同场景里最合适的档位。

通用发布构建,-O2 是稳妥的默认值;循环密集、数值计算、图像处理这一类能明显吃到向量化红利的模块,可以在验证后单独用 -O3;内存和存储紧张的嵌入式环境,用 -Os 压缩代码体积;日常开发调试,则用 -O0 保证断点、变量值和源码的一一对应。Rust 的 opt-level=2/3 底层也是同一套 LLVM 优化路线,选型思路一致。

先按 -O2 发布,再用性能剖析器找出真正的热点,最后只在热点代码上把级别升到 -O3。这就像让疯狂总厨只负责那道招牌菜,其他菜还是交给稳定主厨。

level={−O0,开发调试−O2,通用发布默认−O3,热点循环 / 数值计算−Os,体积敏感场景\displaystyle\text{level} =\begin{cases}-O0, & \text{开发调试}\\-O2, & \text{通用发布默认}\\-O3, & \text{热点循环 / 数值计算}\\-Os, & \text{体积敏感场景}\end{cases}
选型不是拍脑袋,而是先明确你的瓶颈是 CPU、内存还是磁盘体积。
  1. 调试选项开发阶段使用 -O0,优化少,符号映射清晰。
    −O0,开发调试-O0, \text{开发调试}
  2. 默认选项生产构建优先 -O2,在代码规模和性能之间取得平衡。
    −O2,通用发布默认-O2, \text{通用发布默认}
  3. 激进选项确认循环是热点且无精度风险时,再对特定模块用 -O3。
    −O3,热点循环-O3, \text{热点循环}
  4. 体积选项嵌入式或边缘设备关注产物大小,用 -Os。
    −Os,体积敏感-Os, \text{体积敏感}

性能验证

“总厨说这道菜快了三倍”不算数,桌上至少要有一只可靠的秒表。性能优化最忌拍脑袋,-O3 是否值得上线,必须用数据回答:对同一份工作负载,把 -O2 和 -O3 两个版本各跑很多次,记录耗时,用中位数而不是平均值对比,因为平均值容易被一次系统抖动带偏。还可以用 perf 这类剖析器看热点落在哪,用 objdump 或 godbolt.org 看汇编是否真的生成了向量化指令。

基准测试的重复次数和统计口径都很重要。单次运行可能有缓存预热、系统调度等噪音,所以要做重复实验。中位数比平均数更适合作为“典型性能”的代表值,因为它能过滤掉极端噪音,就像评价一位厨师的手艺时,把一次意外打翻锅的成绩丢掉,再看稳定表现。

验证流程应该是:先选一个有代表性的基准测试,再分别编译 -O2 和 -O3 两个版本,最后用中位数和置信区间说话。如果 -O3 的收益只有 1%,却引入精度变化和更大的二进制体积,这笔交易通常不值得做。

T~=median(T1,T2,…,Tn)\displaystyle \tilde{T} = \mathrm{median}(T_1, T_2, \dots, T_n)
用多次运行的中位数作为性能指标,是判断优化级别是否值得用的第一步。
  1. 每次耗时T_i 是第 i 次运行测得的时间样本。
    TiT_i
  2. 中位数符号\tilde{T} 表示这一组样本的中位数,而不是平均值。
    T~\tilde{T}
  3. 中位数定义把所有样本排序后取中间的那个值,能抵抗偶然抖动。
    median(T1,…,Tn)\mathrm{median}(T_1, \dots, T_n)

AI 应用场景

生产构建发布

阶段:发布构建与持续集成

解决的问题:很多团队为了“压榨性能”直接全局开 -O3,结果编译时间变长、二进制变大,线上延迟却没有明显下降。AI 推理服务的 C++ 模块更怕这种不可控变化。正确做法是:默认 -O2 发布,用 profiling 找到热点,只对热点模块单独升 -O3,并把每次优化前后的 P99 延迟做 A/B 对比。

具体案例:某推荐系统的特征服务原先用 -O0 构建,切换成 -O2 后 P99 延迟下降 18%;对 embedding 查询的循环热点单独开 -O3 并启用向量化,又下降了 6%,全程没有出现浮点精度问题。

深度学习推理引擎算子优化

阶段:模型推理与算子部署

解决的问题:矩阵乘、卷积、归一化等算子本质是大循环和数组运算,是 -O3 自动向量化的主战场。但 -O3 可能改变浮点累加顺序,导致推理结果和训练时的数值分布产生细微偏差。AI 工程师需要在“更快推理”和“精度可复现”之间找平衡。

具体案例:一个自研推理引擎在优化 LayerNorm 算子时,对内层循环用 -O3 编译,同时保留一份 -O2 版本做回归测试,要求最大绝对误差小于 1e-6。通过后,单算子耗时降低 22%,模型整体精度没有任何回退。

边缘端模型部署

阶段:边缘设备部署

解决的问题:智能摄像头、嵌入式网关不仅 CPU 弱,Flash 和内存也很小。盲目 -O3 会让可执行文件急剧膨胀,甚至装不进设备。此时优先用 -Os 压缩体积,再用剖析器确认性能是否达标。

具体案例:一个边缘图像预处理模块从 -O2 切到 -Os 后,固件体积减少 22%,帧率几乎不变,因此留出了更多空间给模型权重。

Python 演示

python38 行
"""模拟编译器优化级别对循环求和的影响(纯 Python 演示,非真实编译器)"""import randomimport statisticsimport time def sum_scalar(values):    total = 0    for v in values:        total += v    return total def sum_unrolled(values):    total = 0    n = len(values) // 4 * 4    i = 0    while i < n:        total += values[i] + values[i+1] + values[i+2] + values[i+3]        i += 4    for j in range(n, len(values)):        total += values[j]    return total def median_time(fn, values, repeat=5):    samples = []    for _ in range(repeat):        t0 = time.perf_counter()        fn(values)        samples.append(time.perf_counter() - t0)    return statistics.median(samples) if __name__ == "__main__":    data = [random.random() for _ in range(1_000_000)]    for name, fn in [        ("-O0 朴素循环", sum_scalar),        ("-O2 循环展开", sum_unrolled),        ("-O3 内置 sum", sum),    ]:        print(f"{name}: {median_time(fn, data):.4f} 秒")
目的:用纯 Python 模拟 -O0 朴素循环、-O2 手写循环展开、-O3 风格的内置向量化求和,感受优化思路带来的差异。
  1. def sum_unrolled(values):模拟 -O2 的循环展开优化,一次处理四个元素。
  2. total += values[i] + values[i+1] + values[i+2] + values[i+3]减少循环跳转次数,是展开优化的核心动作。
  3. return statistics.median(samples)用中位数而不是平均数评估性能,避免单次抖动污染结论。
  4. for name, fn in [("-O0 朴素循环", sum_scalar), ...]把不同“优化策略”的函数放在同一基准下对比。
  5. print(f"{name}: {median_time(fn, data):.4f} 秒")用统一格式输出结果,便于直接看到差异。

常见误区

误区我只要把编译选项从 `-O2` 改成 `-O3`,程序肯定会跑得更快,最多就是编译慢一点。

正解-O3 确实会开启更多优化,但循环展开会让代码膨胀,激进的函数内联可能压垮指令缓存,最终表现可能更慢。实际收益通常在 0% 到 5% 之间,有的场景反而倒退。一定要用基准测试对比,而不是凭“级别更高”想当然。

为何常见:人很容易把“优化”理解成“越优化越快”,忽略了现代 CPU 的瓶颈常常是缓存和内存,而不是单纯的计算量。

误区编译器做的优化应该都是等价变换,`-O3` 只是让代码变得更快,不会改变结果。

正解对完全良定义的代码,编译器会遵守“as-if”规则,保证可观察行为基本一致;但浮点数是个特例,运算重排会改变舍入结果。如果代码本身触发了未定义行为,-O3 更会把隐藏问题放大成完全不可预期的行为。

为何常见:大家默认“编译器是严谨的数学工具”,却忘了浮点近似和未定义行为让很多等价变换不再等价。

误区开启 `-ffast-math` 后结果只是更近似,AI 模型训练时不也经常用 float 吗?应该没关系。

正解fast-math 不仅提高近似程度,它假设程序里没有 NaN、没有 Inf、忽略符号零,还允许把除法和重排按“数学理想情况”处理。在极端数据下,可能让判断分支、循环收敛条件发生质变,精调过的模型也可能直接掉点。必须用数据集回归验证。

为何常见:深度学习中 float 本身就是近似,很多人已经把“近似”当成了“无所谓”。

误区性能要求高的项目必须 `-O3`,否则就是浪费 CPU。

正解大多数项目用 -O2 已经能获得绝大部分收益,还避免了代码膨胀和数值风险。-O3 的正确用法是先定位热点,再针对热点模块开高档优化,最后用基准测试验收。最高级别不等于最优选择。

为何常见:网上很多 benchmark 只展示孤立循环的 -O3 收益,忽略了真实程序中的缓存、内存带宽和编译时间成本。

面试题

Q1. -O3 和 -O2 的关系是?

单选题 · easy

  • A.包含O2并追加激进优化← 正确答案
  • B.与O2完全独立互不相关
  • C.比O2开启的优化点更少
  • D.只影响调试信息格式

答案:A。在 GCC 中,-O3 通常包含 -O2 的全部优化,再追加向量化、循环展开等激进手法。B 错在两者不是平行选项;C 说反了;D 错在 -O 影响的是代码生成性能,而不是调试信息。

Q2. 日常开发调试时,最合适使用哪个优化级别?

单选题 · easy

  • A.最合适的是 -O0← 正确答案
  • B.最合适的是 -O1
  • C.最合适的是 -O2
  • D.最合适的是 -O3

答案:A。-O0 关闭大部分优化,变量、断点与源码行号对应关系最直接,适合开发调试。-O1/-O2/-O3 会重排或合并指令,导致调试时“变量看不到”或“断点跳来跳去”。

Q3. -O3 的自动向量化最受益于哪类代码?

单选题 · easy

  • A.大量循环和数组运算← 正确答案
  • B.大量字符串拼接操作
  • C.大量虚函数调用操作
  • D.大量异常抛出场景

答案:A。自动向量化把循环中对数组的标量操作变成 SIMD 多路并行操作。字符串拼接、虚函数调用和异常处理更多依赖库和运行时,向量化收益有限。

Q4. 默认不开 fast-math 时,编译器对浮点运算通常怎么做?

单选题 · mid

  • A.基本保持源码求值顺序← 正确答案
  • B.任意重新结合运算
  • C.使用近似倒数代替除法
  • D.自动忽略 NaN 和 Inf

答案:A。默认尊重 IEEE 754 语义时,编译器不会随意把浮点加法重新结合,也不会假设 NaN 不存在。B、C、D 是 -ffast-math 或近似数学带来的典型行为。

Q5. 开启 -ffast-math 可能带来哪个后果?

单选题 · mid

  • A.精度与异常行为改变← 正确答案
  • B.代码体积一定变小
  • C.编译时间一定缩短
  • D.程序一定更加安全

答案:A。fast-math 假设没有 NaN、Inf,并允许重排浮点运算,所以结果精度和异常行为都会改变。它与代码体积、编译时间没有必然关系,更不会让程序更安全。

Q6. -O3 有时比 -O2 慢,最常见原因是?

单选题 · hard

  • A.循环展开导致缓存压力← 正确答案
  • B.编译器版本太低导致
  • C.使用了统一内存导致
  • D.缺少调试符号导致

答案:A。激进循环展开和函数内联会让二进制体积增大,指令缓存命中率下降,最终整体变慢。编译器版本、内存类型和调试符号都不是 -O3 特有的风险来源。

Q7. 关于 -O3 的风险,以下哪些说法正确?

多选题 · mid

  • A.可执行文件可能变大← 正确答案
  • B.暴露隐藏未定义行为← 正确答案
  • C.一定比O2跑得更快
  • D.浮点结果可能改变← 正确答案

答案:ABD。-O3 的展开和内联会增大代码体积,激进分析也可能让未定义行为显形,浮点重排会改变结果。它并不保证比 -O2 快,所以 C 错。

Q8. 以下哪些是 -O3 比 -O2 更常见的典型优化?

多选题 · hard

  • A.使用自动向量化← 正确答案
  • B.进行循环展开← 正确答案
  • C.生成调试符号
  • D.消除无用代码

答案:AB。-O3 在 -O2 基础上新增自动向量化和更激进的循环展开。调试符号生成与优化级别无关;死代码消除在 -O2 甚至更早的级别就已经做了。

Q9. 验证 -O3 是否值得上线,最可靠的手段是?

单选题 · hard

  • A.基准测试加剖析器对比← 正确答案
  • B.查看编译器官方文档
  • C.询问有经验同事意见
  • D.随机切换不同优化级别

答案:A。性能优化必须以数据和场景为准。基准测试给出量化对比,剖析器帮助解释为什么变快或变慢。文档和别人的经验只能作为参考,不能替代实际测量。

Q10. 在数值敏感项目中,哪些措施能降低 -O3 的精度风险?

多选题 · extreme

  • A.关闭 FMA 融合选项← 正确答案
  • B.使用 -ffast-math 选项
  • C.显式分组并验证误差← 正确答案
  • D.只信任汇编输出结果

答案:AC。FMA 融合会改变舍入行为,关闭后可保留更传统的计算顺序;显式分组和误差验证能有效防回归。fast-math 反而放大风险,只看汇编也不能替代数值正确性验证。