CHAPTER 52 / Compiler 与 runtime
ABI、装载与 runtime:找到函数还不等于能安全调用
编译产物怎样接上参数、设备和资源生命周期?
这一章要弄清楚
- 解释符号、ABI与目标兼容的区别
- 检查参数布局和装载合同
- 把代码与buffer生命延长到异步完成
先备知识:真实资源与RAII:独占拥有 / 多文件编译、链接与构建 / 进程、系统调用与文件描述符 / ROCm 各层:一次请求由谁负责 / Tensor lowering 与内存规划:值什么时候可以共用一块空间
C++20 本机逻辑模型;不是设备仿真或性能测量。厂商语法片段未在设备或 SDK 执行,实际 API 以匹配版本的公开文档为准。
编译结束只是下一段流程的输入
编译器产生目标代码后,还需要有人选择产物、解析符号、准备参数和资源、发起执行并处理完成。这一段把compiler与runtime连接起来。函数名字存在,不代表目标机器能执行它;指令集、目标特性、代码格式、runtime版本及参数合同都可能不匹配。
符号可以理解为链接和查找使用的名字,ABI是不同编译单元或调用双方约定的二进制接口。它可能涉及参数放在哪里、大小和对齐、调用约定、返回值以及特定对象格式。源代码里都写一个int,并不能独自证明两个独立系统使用兼容的调用方式。
把参数看成明确布局
例一定义一个完全原创的教学参数块:前四字节保存无符号数量,中间四字节填零,后八字节保存一个数值token,均按小端编码。数量三、token二百五十八被装入十六字节后,再由解码器读回。它不是HIP、HSA、Cerebras或Tenstorrent的真实ABI,更不是可传给kernel的参数包。
从32位拆装扩展到64位字段(补充,另估25分钟)
先用 SYS-00 S2 的无符号移位、窄化与小端拆装,以及 IR-45 的移位位宽边界。这里新增的是例一中 uint64_t 的8字节字段和16字节位置合同,不重新定义位运算。
本例要求实现提供精确的 uint8_t、uint32_t、uint64_t 类型。bytes{} 将16个字节初始化为零。编码 token 时,i 依次为0–7,8*i 依次为0、8、16、24、32、40、48、56;token >> (8*i) 把当前一档移到最低位,再转成 uint8_t,只保留低8位。对64位无符号左操作数,移56位在范围内,移64位不合法;本文件的循环不会取到64。
解码这一行要从括号内往外读:static_cast<std::uint64_t>(bytes[8+i]) << (8*i)。先把一个字节转成64位无符号数,再移到该档位置,最后用已学的 |= 并入 decoded_token。若只把完整移位结果最后转成64位,前面的运算可能已经在提升后的 int 宽度上发生,不能补救较大移位量的问题。数量字段采用相同步骤,但宽度为32位,移位量只到24。
**纸上走一次:**输入 n=3、token=258,逐栏写出数组内容,再按相同偏移读回:
| 字节偏移 | 0–3:数量 | 4–7:保留位 | 8–15:token |
|---|---|---|---|
| 十进制字节值,按偏移递增 | 3,0,0,0 |
0,0,0,0 |
2,1,0,0,0,0,0,0 |
| 回读或检查 | decoded_n=3 |
每项仍为0 | decoded_token=2+1×256=258 |
保留的四个零来自初始化且未被编码循环覆盖,不是编译器自动插入的 struct padding。token 是本题的数值标记,不是一个真实设备指针。例一最终应输出 size=16 n=3 token=258;这验证自定 CPU 字节格式,不能证明任一家设备接受该参数布局。语言依据是 C++ 移位规则与整数转换规则,核对日期 2026-09-08。
这个例子解释为什么不能直接把任意C++ struct的原始字节当跨平台协议。Padding、alignment、整数宽度、字节序和指针含义都需要合同。真实ABI应来自对应目标文档和工具链;教学模型用显式编码避免依赖本机struct布局。
先把258拆成字节,再安排位置
本节以SYS-00 S2已授的小端规则与258拆装为先备;以下在已授规则上展开本题16字节参数布局,258与513的算式属于该格式的应用。64位token循环的读法见上方32位到64位的扩展。
“小端”怎样变成可以手算的步骤?本题约定一个字节恰好8位,可表示0到255。先把非负整数写成以256为一档的数位:最右一档的权重是1,下一档是256,再下一档是256乘256。这里的“低”指权重小,不是在图上画得低,也不是数值本身比较小。
258可以写成1乘256再加2,所以低有效字节是2,下一字节是1。也可以按重复除法算:258除以256得商1、余数2,先取余数2;再把商1除以256,得商0、余数1,取1。已经没有剩余数值,若格式固定八字节,后面六个位置补0。反向检查:2乘1加1乘256,正好还原258。
小端编码(little-endian)把低有效字节放在较小偏移处,因此本题token区域从低地址向高地址依次是下面的十进制字节值。偏移是相对整份参数块开头的位置,token从偏移8开始:
| 参数块偏移 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|
| token字节 | 2 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
数值3的数量字段同理:偏移0至3为3、0、0、0;偏移4至7按题目格式填零。这说明“怎样拆数”与“怎样放位置”是两个步骤。若采用大端顺序,同样的数位会把高有效字节放在较小偏移处;本题没有采用它。这里每个表格项是一个字节的数值,不是字符’2’或字符’1’的编码。
换一个数自己推演:513的八字节小端表示是什么?513等于2乘256加1,故依次为1、2、0、0、0、0、0、0,反向还原得到513。示例源码用移位和无符号整数转换实现相同分解;这些运算的读法由系统入口SYS-00 S2负责,不能把这张表当作那部分语言讲授已经完成。这里解释的是题目自定格式,不推断任何真实厂商ABI;真实LLVM数据布局中的大小端标记可核对 LLVM Language Reference 的Data Layout。
符号解析还需要错误路径
例二用C++函数表表达“按名字找到可调用对象”。twice能够把七变成十四,missing明确被拒绝。它没有从文件装载机器代码,也没有实现JIT或动态链接;只展示查找失败不能被默默当成一个有效入口。
LLVM的ORC/JITLink公开资料讨论运行时链接、符号解析和资源管理;AMDGPU后端文档则说明目标代码及元数据相关合同。它们关注层次不同,不能用一个CPU函数表证明GPU产物可装载。读真实工具日志时,应区分找不到符号、目标不兼容、参数错误和执行失败。
一次安全调用要共同满足什么
调用前确认正确产物、正确设备、正确入口以及完整参数布局;参数引用的buffer必须存在、可访问且容量足够。调用返回后还需区分已提交与已完成,不能把代码模块或其使用的数据提前释放。异步工作可能仍会访问常量、函数或临时资源。
代码缓存也有同样问题。Cache key不能只有源码字符串,还可能需要目标架构、编译选项、shape、dtype、layout及相关版本。如果使用旧key取出不匹配产物,错误可能在装载阶段出现,也可能延迟成数值或访问错误。缓存加速必须建立在兼容性合同之上。
让错误回到最早可检查的位置
未知符号应在查找时拒绝;参数长度不匹配应在提交前拒绝;不支持的目标应在选择或装载时报告。越早检测,越能保留具体上下文。若所有错误都拖到设备崩溃再返回一个模糊状态,使用者很难定位到底是编译器、runtime还是输入合同问题。
实际工程还需要处理模块卸载、资源回收和并发调用。等待所有使用者完成后再回收是一种清晰起点;更复杂的引用计数或资源追踪也要证明相同生命条件。不要把“对象析构了”误当成“设备不再使用了”。
用这一章串起整条路线
C++提供类型与资源表达,compiler把程序转成目标产物,runtime组织执行,设备完成计算,测试与测量检查结果和成本。面试可以沿一份输入从源代码一直讲到回读,并指出每个边界由谁负责。本章完成意味着能解释这些合同;真实loader、JIT、driver和设备执行仍是各自需要独立实现与验证的更深主题。
编译产物接上调用合同
自定义布局准备数量和token。
函数表查找twice。
找到入口才调用。
阅读完整推演文字
- 参数
n@0:3;token@8:258;bytes:16
自定义布局准备数量和token。
- 解析
symbol:twice;argument:7
函数表查找twice。
- 执行
result:14;missing:拒绝
找到入口才调用。
跟着例子,走完一遍
显式编码教学参数块
自定义16字节布局:u32数量3在0;u64 token258在8。
- 零初始化padding
- 逐字节小端编码
- 独立解码并核对
// Original CPU teaching model; not a device simulator or benchmark.
#include <iostream>
#include <array>
#include <cstdint>
#include <cstddef>
#include <stdexcept>
void require(bool ok) { if (!ok) throw std::runtime_error("model check failed"); }
int main() {
std::array<std::uint8_t,16> bytes{};
const std::uint32_t n=3;const std::uint64_t token=258;
for(std::size_t i=0;i<4;++i) bytes[i]=static_cast<std::uint8_t>(n>>(8*i));
for(std::size_t i=0;i<8;++i) bytes[8+i]=static_cast<std::uint8_t>(token>>(8*i));
std::uint32_t decoded_n=0;std::uint64_t decoded_token=0;
for(std::size_t i=0;i<4;++i) decoded_n|=static_cast<std::uint32_t>(bytes[i])<<(8*i);
for(std::size_t i=0;i<8;++i) decoded_token|=static_cast<std::uint64_t>(bytes[8+i])<<(8*i);
require(decoded_n==3 && decoded_token==258);
for(std::size_t i=4;i<8;++i) require(bytes[i]==0);
std::cout<<"size="<<bytes.size()<<" n="<<decoded_n<<" token="<<decoded_token<<'\n';
}
size=16 n=3 token=258。
只是自定义序列化合同,不是任何厂商真实ABI。
在本机运行这个例子
下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 48-a.cpp -o example && ./example预期标准输出:
size=16 n=3 token=258
符号查找成功与失败
函数表只有twice;用7调用,再查missing。
- find检查入口存在
- 调用已知函数得到14
- 未知名称拒绝
// Original CPU teaching model; not a device simulator or benchmark.
#include <iostream>
#include <map>
#include <string>
#include <functional>
#include <stdexcept>
void require(bool ok) { if (!ok) throw std::runtime_error("model check failed"); }
int main() {
const std::map<std::string,std::function<int(int)>> table{{"twice",[](int x){return x*2;}}};
const auto found=table.find("twice");
require(found!=table.end());
const int value=found->second(7);
const auto missing=table.find("missing");
require(value==14 && missing==table.end());
std::cout<<"result="<<value<<" missing=rejected\n";
}
result=14 missing=rejected。
函数表不是动态loader,但展示显式失败合同。
在本机运行这个例子
下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 48-b.cpp -o example && ./example预期标准输出:
result=14 missing=rejected
找到名称就直接发起调用
忽略目标、参数布局、buffer和模块生命周期。
修正思路:把符号、兼容性、参数与完成条件分别检查。
轮到你动手
先写预测或代码,再按需打开提示。完整答案用于对照自己的推理。
练习 1
把数量字段从u32改成u64,调用双方只改一边会怎样?
给我一点提示
- 布局和解码必须一致
- 后续offset可能变化
查看答案与推理
一侧按八字节读写,另一侧仍按四字节解释,可能读到padding或覆盖下一字段。必须版本化合同并同时调整编码、解码、大小与offset检查,不能只改类型名。
练习 2
设计产物cache key的最小候选集合并解释。
给我一点提示
- 列出会改变生成代码的输入
- 不要把无关信息混入
查看答案与推理
至少考虑源/IR身份、编译选项、目标架构与特性、工具链/产物格式版本;形状专门化时加入shape、dtype和layout。实际集合由编译合同决定,测试应确保不同关键输入不会误用同一产物。
动手看真实 Clang / LLVM IR / 汇编与链接 →
把理解说出来
先用中文讲清因果,再用英文回答。问题依据技能主题编写,并非公司内部题库。
符号存在为什么还可能不能调用?
参考回答 / English answer
目标架构、格式、ABI、参数和资源可能不匹配,名字只是一个检查。
A symbol name does not establish compatibility. Target code, ABI, arguments, and resources must also match.能把任意struct原始字节直接传给另一端吗?
参考回答 / English answer
不能无条件这样做,padding、alignment、宽度、字节序与指针语义需一致。
Raw struct bytes are not a portable contract by themselves. Layout, endianness, widths, and pointer meaning must be defined.本例token258的小端前两个字节是什么?
参考回答 / English answer
低字节2,高字节1;其余六字节零,但仅适用于本题自定义编码。
The first two bytes are two and one. This follows our explicitly defined little-endian teaching format.异步调用返回后能卸载模块吗?
参考回答 / English answer
不能只凭返回判断,必须确保所有相关执行与资源使用完成。
Submission returning does not imply completion. The module must remain valid for every outstanding use.编译缓存只用源码作为key够吗?
参考回答 / English answer
往往不够,还需target、flags、shape/dtype/layout和版本等决定产物的输入。
Source text alone may not identify a compatible artifact. Target, options, shapes, layouts, and relevant versions can matter.未知符号应在哪里报告?
参考回答 / English answer
查找阶段立即拒绝并保存上下文,不制造默认地址或拖到执行崩溃。
I reject an unresolved symbol during lookup. Early errors preserve clearer context than a later execution failure.继续查证
- LLVM ORC design ↗
Runtime linking, symbol lookup and resource management
- LLVM AMDGPU usage ↗
Code objects, target identification and kernel argument metadata
- LLVM JITLink ↗
Object linking and symbol resolution
公开资料用于查证;本章图解和例题是独立教学内容。CPU 逻辑模型不能证明设备性能。
接着看已有的图解
- ROCm 内核与 KFD 主题库 ↗
继续了解队列、内存和内核边界,避免把稳态直接派发描述为每次都进入内核。
这些资料按主题补充本章内容。