CHAPTER 52 / Compiler 与 runtime

ABI、装载与 runtime:找到函数还不等于能安全调用

编译产物怎样接上参数、设备和资源生命周期?

阅读与推演约 55 分钟练习时间另计

这一章要弄清楚

  • 解释符号、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_tuint32_tuint64_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=3token=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和设备执行仍是各自需要独立实现与验证的更深主题。

观察 · 推演

编译产物接上调用合同

n@03token@8258bytes1601 / 03 · COMPILERn@03token@8258bytes1601 / 03 · COMPILER
参数

自定义布局准备数量和token。

1 / 3
阅读完整推演文字
  1. 参数

    n@0:3;token@8:258;bytes:16

    自定义布局准备数量和token。

  2. 解析

    symbol:twice;argument:7

    函数表查找twice。

  3. 执行

    result:14;missing:拒绝

    找到入口才调用。

跟着例子,走完一遍

例题 01C++20 · 本机可运行

显式编码教学参数块

自定义16字节布局:u32数量3在0;u64 token258在8。

  1. 零初始化padding
  2. 逐字节小端编码
  3. 独立解码并核对
48-a.cpp
下载
// 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';
}

如何编译和运行下载的 .cpp 文件 →

结果与解释

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
例题 02C++20 · 本机可运行

符号查找成功与失败

函数表只有twice;用7调用,再查missing。

  1. find检查入口存在
  2. 调用已知函数得到14
  3. 未知名称拒绝
48-b.cpp
下载
// 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,调用双方只改一边会怎样?

给我一点提示
  1. 布局和解码必须一致
  2. 后续offset可能变化
查看答案与推理

一侧按八字节读写,另一侧仍按四字节解释,可能读到padding或覆盖下一字段。必须版本化合同并同时调整编码、解码、大小与offset检查,不能只改类型名。

练习 2

设计产物cache key的最小候选集合并解释。

给我一点提示
  1. 列出会改变生成代码的输入
  2. 不要把无关信息混入
查看答案与推理

至少考虑源/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.

继续查证

公开资料用于查证;本章图解和例题是独立教学内容。CPU 逻辑模型不能证明设备性能。

接着看已有的图解

这些资料按主题补充本章内容。