首次编译 .cpp 请先看保存、编译和运行教程;本页在此基础上观察 IR 与链接。
LOCAL TOOL LAB / EXECUTED ON CPU
一次真实的 Clang → LLVM IR → 可执行文件实验
先备:基础17编译链接及 IR-45 I1–I5完整读法。本页只安排实验与复现;语言构造、IR记号及其边界由章节讲授。用现有 CPU 和 Clang 即可;不需要 GPU、opt 或新设备。本包独立编写,实验对象是普通 C++ 函数,没有实现 LLVM pass,也没有运行 MLIR。
先定义行为,再谈优化
本实验接口:transform(x) 计算 x * 2 + 3 + x * 0,源码实际使用 unsigned,测试先确认它具有32个值位。按 I4的固定宽度合同先填写这五个输入:0→3,1→5,7→17,2147483648→3,4294967295→1;保留全部边界检查。
沿用 I5的宽整数oracle与掩码读法,核对 check.cpp 的独立期望值;被测接口和输入约定保持一致,不能仅换成 int 后沿用原证明。不要调用被测函数来生成期望值。
1 · 先运行整个实验
在解压后的 compiler-lab 目录运行:
clang++ --version
python3 run.py
脚本使用临时目录编译,不改动源文件;把可阅读产物写到 output/。可以显式选择编译器:CXX=clang++ python3 run.py。本书记录的环境是 Apple Clang 17、arm64 macOS。Linux 上安装了 Clang 和相应 C++ 标准库也可尝试;指令名称与汇编应以你的机器为准,不能拿本书截图式输出代替自己的记录。
2 · 停在 LLVM IR 层
clang++ -std=c++20 -O0 -S -emit-llvm arithmetic.cpp -o O0.ll
clang++ -std=c++20 -O2 -S -emit-llvm arithmetic.cpp -o O2.ll
选项与完整函数头读法见 I5。打开自己生成的 O0.ll,按 I1的记号表与 I3的SSA值/内存双表,从 define ... @transform 追踪输入7到返回17。记录文件中的实际名字,不重命名原始产物。
再按 I4的移位与边界表读 O2.ll,记录这次实际留下的操作。本书保留产物是左移一位、加3、返回;未来Clang输出以实际文件为准,不能把行数写死为正确性条件。
应用 I5的等价性推导:用本实验接口说明 dead 消失后为何仍应满足五个期望。把推导与实际执行结果分开记录。
3 · IR 确实能继续编译
clang++ -c O2.ll -o from-ir.o
clang++ -std=c++20 check.cpp from-ir.o -o check-ir
./check-ir
这一步把刚才生成的 IR 重新送进工具链,再与测试入口链接。run.py 对 O0、O2 都执行这个回路,并与直接从 C++ 编译的结果对比。它证明这些 .ll 是实际可处理的中间产物,不是为了画图手写的伪 IR。
4 · 找到链接器的责任边界
clang++ -std=c++20 -O2 -c arithmetic.cpp -o arithmetic.o
clang++ -std=c++20 check.cpp arithmetic.o -o check
./check
clang++ -std=c++20 check.cpp -o missing-definition
最后一条应失败:check.cpp 里有 transform 的声明,所以编译器能检查调用方式;链接时却没有找到函数定义。修复是把正确目标文件交给链接器。随意补一个永远返回 0 的函数会让链接通过,却破坏行为。再跑原有边界测试才能发现这个区别。
extern "C" 的语言链接及平台/类型边界由 I5讲授;在这里核对声明、目标文件与当前工具链是否匹配。
5 · 从本机产物连接到 runtime
用 clang++ -O2 -S arithmetic.cpp -o O2.s 查看汇编。当前 arm64 产物与 x86 机器可能很不一样;汇编是面向目标架构的表达,IR 则还包含较多跨目标的结构。最终执行程序时,操作系统的加载机制让代码和数据可被进程使用;这与厂商 runtime 管理队列、设备缓冲、事件不是同一个层次。
本实验完成了 CPU 编译、链接和执行;它没有测试设备代码打包、GPU module 加载、tensor lowering 或设备内存规划。把这条真实的小链路弄清楚后,再回第 47–48 章比较 accelerator runtime 多出来的责任。
独立改写与检查
- 将接口改成
x * 4 + 1,先写出上述五个输入的新期望值,再修改实现和独立 oracle;同时把 run.py 的 expected 输出夹具更新为这五行:0→1、1→5、7→29、2147483648→1、4294967295→4294967293。脚本仍保留旧输出时会按设计拒绝新行为,不能只删除检查。预测 O2 IR 会如何变化,最后用真实产物验证。不要只修改测试使失败消失。 - 新增一个
square函数,在不同目标文件中定义与调用;先故意漏掉定义,记录链接失败,再补齐并测试 0、1、65536、UINT_MAX。乘法结果按约定回绕,oracle 要用足够宽的中间类型。 - 面试口述:源码行、IR 值、目标指令、链接符号分别是什么?为什么 O0/O2 输出相同不等于已经证明所有输入都正确?为什么这个实验不能当作 GPU profiling 经历?
参考与验证范围
- Clang 命令说明:Stage Selection Options、Code Generation Options,用于核对编译阶段与选项。
- LLVM Language Reference:Integer Type、Binary Operations、Function Definitions,用于核对 IR 的值和算术语义。
- Kaleidoscope:生成 LLVM IR:下一步学习 IR 构造接口;本实验没有实现该教程编译器。
资料核对:2026-09-05。脚本检查 O0/O2 的直接执行、IR 回编译执行、ASan/UBSan 运行、缺定义链接拒绝。五个边界值不穷尽 2^32 个输入;测试结果与语言层面的模运算推导互相补充。原始 IR、汇编和验证记录由你运行脚本后生成,生成文件不应混成手写源码。
文档迁移:2026-09-08将先备讲解收束到IR-45;本次README修改没有重新运行旧实验,也没有更新旧执行收据的时间。旧源码、runner与O0/O2产物按原身份保留。
唯一源码:arithmetic.cpp
// Unsigned arithmetic has defined modular wraparound; this contract matters to optimization.
extern "C" unsigned transform(unsigned x) {
const unsigned doubled = x * 2u;
const unsigned offset = doubled + 3u;
const unsigned dead = x * 0u;
return offset + dead;
}保留的真实编译产物:两个函数体
整读下面的函数声明与属性前,先完成IR-45 I5:编译器输出与属性。这里复用已有执行记录及原始IR文件,不代表本轮重新生成或运行。
Apple clang version 17.0.0 (clang-1700.0.13.5)。以下直接读取保留的原始工具产物;省略函数体之外的 target/attributes/module metadata。
O0.ll
define i32 @transform(i32 noundef %0) #0 {
%2 = alloca i32, align 4
%3 = alloca i32, align 4
%4 = alloca i32, align 4
%5 = alloca i32, align 4
store i32 %0, ptr %2, align 4
%6 = load i32, ptr %2, align 4
%7 = mul i32 %6, 2
store i32 %7, ptr %3, align 4
%8 = load i32, ptr %3, align 4
%9 = add i32 %8, 3
store i32 %9, ptr %4, align 4
%10 = load i32, ptr %2, align 4
%11 = mul i32 %10, 0
store i32 %11, ptr %5, align 4
%12 = load i32, ptr %4, align 4
%13 = load i32, ptr %5, align 4
%14 = add i32 %12, %13
ret i32 %14
}O2.ll
define noundef i32 @transform(i32 noundef %0) local_unnamed_addr #0 {
%2 = shl i32 %0, 1
%3 = add i32 %2, 3
ret i32 %3
}实际标准输出
0 -> 3
1 -> 5
7 -> 17
2147483648 -> 3
4294967295 -> 1
这些是 CPU 工具链结果,没有运行 GPU、MLIR 或自定义 LLVM pass。