CHAPTER 50 / Compiler 与 runtime
优化首先要合法:别名、溢出与浮点重排
少一次读取,为什么反而算错了?
这一章要弄清楚
- 用反例检查优化等价性
- 区分signed/unsigned及IR承诺
- 解释alias分析与vectorization条件
先备知识:指针:地址、空值与有效访问 / 原子操作与 happens-before / CPU cache、缓存一致性与 SIMD / AST、IR 与 SSA:同一个表达式的三种表示
C++20 本机逻辑模型;不是设备仿真或性能测量。厂商语法片段未在设备或 SDK 执行,实际 API 以匹配版本的公开文档为准。
优化不是把代码写得看起来更短
编译器优化必须保留语言合同要求的行为。把两次相同地址读取合并成一次,看似省事;但如果中间有写入,第二次值可能已经变化。优化前先问“哪些输入和执行是合法的”,再证明在这些前提下新旧程序等价,最后讨论收益。
例一函数先读p得到old,再通过q写入5,最后返回old加p当前值。若p和q指向同一个初始为2的对象,正确答案是2加5等于7;若错误地用第一次读取代替第二次,答案成为4。两条指针的类型和值关系,不应在没有依据时假定互不重叠。
别名信息为什么影响性能
Alias表示不同访问路径可能指向同一存储。编译器不能排除这种情况时,某些load消除、循环重排或vectorization需要更保守,或者生成运行期检查,选择安全的快速路径与通用路径。解决方法不是随便添加“不别名”承诺,而是让接口、数据组织和实际调用满足其要求。
int observe(int* p, int* q) {
const int old = *p;
*q = 5;
return old + *p;
}
// 若 p==q,最后一次读取不能用old替换。
该例取值很小,不涉及算术溢出;它单独暴露内存依赖。好的优化测试会像这样一次隔离一个原因,而不是把别名、线程和浮点误差混在一个大程序里。
数学恒等式不自动适用所有类型
在无界整数数学中,x加一总大于x。但无符号整数按固定宽度模运算,最大值加一会回到零。例二使用uint32_t,普通值时比较为真,最大值时为假。Signed overflow在C++中是未定义行为,不能为了“验证溢出”直接运行有符号越界再把输出当可靠结果。
LLVM IR中的普通整数运算和带nsw、nuw标记的运算具有不同合同。额外标记是对允许执行的承诺,不是无条件加速开关。前端和优化pass只有在证明成立时才能引入;错误承诺可能让后续推导建立在错误前提上。
浮点重排也需要前提
浮点加法受有限精度与舍入影响,先把大正数与大负数相消,再加小数,可能与先把小数加到大数上不同。NaN、无穷、有符号零以及异常语义也可能影响变换是否合法。Fast-math一类选项允许更宽的假设,但使用者必须明确数值需求,不能只因为benchmark变快就默认接受。
对reduction尤其如此:树形合并改变结合顺序。应预先定义误差指标与非有限值处理,保留严格baseline,不把所有差异都叫bug,也不把任意差异都归为正常舍入。正确的讨论是某个变换在什么合同下被允许。
怎样验证一个优化假设
先列反例类别:指针重叠、不整除长度、零和边界值、非有限浮点、不同优化级别。接着检查优化remarks或IR变化,确认编译器究竟做了什么,再测目标负载。看到-O3比-O2慢也不矛盾,因为代码体积、寄存器压力、访存和分支行为都可能改变。
本章例子都在定义良好的小范围或无符号算术中运行,不靠触发UB产生戏剧化输出。面试时能说出一个具体合法性反例,并给出修正前提,比只列constant folding、CSE、loop unrolling的名字更能体现理解。
同一地址上的两次读取
p与q是同一对象的两个访问路径。
通过q写5;先前复制到old的2不随对象改变。
必须读取新值,而非复用old。
阅读完整推演文字
- 第一次读取
object:2;old:2
p与q是同一对象的两个访问路径。
- 写入
object:5;old:2
通过q写5;先前复制到old的2不随对象改变。
- 第二次读取
correct:2+5=7;cached:2+2=4
必须读取新值,而非复用old。
跟着例子,走完一遍
别名使第二次读取改变
p与q指向同一个初始值2。
- 先读old=2
- 通过q写5
- 正确返回2+5,错误缓存返回2+2
// Original CPU teaching model; not a device simulator or benchmark.
#include <iostream>
#include <stdexcept>
void require(bool ok) { if (!ok) throw std::runtime_error("model check failed"); }
int original(int* p,int* q){const int old=*p;*q=5;return old+*p;}
int cached(int* p,int* q){const int old=*p;*q=5;return old+old;}
int main() {
int a=2,b=2;const int good=original(&a,&a),bad=cached(&b,&b);
require(good==7 && bad==4);
std::cout<<"original="<<good<<" cached="<<bad<<'\n';
}
original=7 cached=4。
演示错误变换;代码本身没有越界或溢出。
在本机运行这个例子
下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 46-a.cpp -o example && ./example预期标准输出:
original=7 cached=4
无符号边界反例
比较uint32_t的x+1>x,x=7及最大值。
- 固定32位无符号类型
- 定义良好的模加法
- 检查最大值回绕
// Original CPU teaching model; not a device simulator or benchmark.
#include <iostream>
#include <cstdint>
#include <limits>
#include <stdexcept>
void require(bool ok) { if (!ok) throw std::runtime_error("model check failed"); }
bool increases(std::uint32_t x) {
const std::uint32_t next=x+std::uint32_t{1};return next>x;
}
int main() {
const bool ordinary=increases(7),edge=increases(std::numeric_limits<std::uint32_t>::max());
require(ordinary && !edge);
std::cout<<"ordinary="<<ordinary<<" at_max="<<edge<<'\n';
}
ordinary=1 at_max=0。
不能把无界数学恒等式无条件搬入固定宽度整数。
在本机运行这个例子
下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 46-b.cpp -o example && ./example预期标准输出:
ordinary=1 at_max=0
用优化标记代替证明
无依据声明不别名或不溢出以获得更短代码。
修正思路:先明确真实调用与类型合同,证明或检查前提,再应用变换。
轮到你动手
先写预测或代码,再按需打开提示。完整答案用于对照自己的推理。
练习 1
把例一改成p和q指向不同对象,两个实现结果是多少?
给我一点提示
- p初值2不变
- q写5不影响p
查看答案与推理
两者都返回4,因为第二次*p仍为2。说明缓存变换可能在明确不别名条件下合法,但不能从一个不重叠测试推广到所有调用。
练习 2
为循环优化列四种测试类别,并解释作用。
给我一点提示
- 覆盖别名和尾部
- 不要运行有符号UB
查看答案与推理
指针重叠检查内存依赖;长度0、1及非向量宽度整数倍检查边界;合法算术边界检查类型合同;浮点NaN/Inf及大小差异悬殊输入检查数值前提。每类应有预先定义的期望行为。
动手看真实 Clang / LLVM IR / 汇编与链接 →
把理解说出来
先用中文讲清因果,再用英文回答。问题依据技能主题编写,并非公司内部题库。
两次*p之间有*q=5,为什么不能总合并load?
参考回答 / English answer
q可能与p别名,第二次读取看到新值;需证明不重叠或分析写入。
The store through q may change the value read through p. Load elimination needs a valid alias and dependency argument.p==q且初值2,例一为什么得7?
参考回答 / English answer
第一次读2,写入5后第二次读5,因此返回7。
The first load observes two. The second observes five after the aliasing store, so the result is seven.uint32最大值加一大于自己吗?
参考回答 / English answer
不大于,按无符号模算术回到零;与C++有符号溢出不同。
Unsigned addition wraps modulo the type's range. The maximum value therefore becomes zero.nsw能随便加来帮助优化吗?
参考回答 / English answer
不能,它声明特定不溢出语义,错误标记会使优化推导失效。
No-signed-wrap is a semantic promise. It must be justified rather than added as a performance hint.为什么树形浮点sum可能与顺序sum不同?
参考回答 / English answer
加法结合顺序与舍入改变;需数值合同及非有限值处理。
Changing the addition tree changes rounding. I define the error contract and handle non-finite values explicitly.代码没有vectorize,下一步如何查?
参考回答 / English answer
检查优化remarks、依赖、别名、循环边界和目标支持,而非直接宣告编译器差。
I inspect optimization remarks and loop dependencies. Aliasing, bounds, and target support may legitimately block vectorization.继续查证
- LLVM vectorizers ↗
Runtime pointer checks and optimization diagnostics
- LLVM Language Reference ↗
Integer flags and fast-math semantics
公开资料用于查证;本章图解和例题是独立教学内容。CPU 逻辑模型不能证明设备性能。