Repository metrics
- Stars
- (31 stars)
- PR merge metrics
- (Avg merge 6d 22h) (4 merged PRs in 30d)
Description
方案摘要
Machina 目前只有一条同步翻译路径:Guest 指令被解码成 IR,随后经过
optimize -> liveness -> regalloc/codegen 生成 Host 代码。冷代码也要支付
这笔编译开销,热点代码则一直停留在基本块级别,缺少跨 TB 优化。
本方案把执行引擎拆成三个层级:
- T0 直接解释已经解码的 Guest 指令,不生成 Host 代码;
- T1 同步生成基线 TB,要求编译快,并且在任何时候都能运行;
- T2 在后台编译热点 Region,承担跨 TB 的寄存器保留和较重优化。
新代码从 T0 开始。达到阈值后生成 T1;T1 的 profile 稳定后才能排队生成 T2。T0 不能直接晋升到 T2。T2 编译失败、过期或被回收时,执行流退回 T1; T1 不可用时退回 T0。
性能目标和测量口径见第 8 节。#31 中的历史数据只用于定位问题,不直接作为 本方案的基线。
1. 设计目标
ISA coverage 由生成器和构建检查保证。新增 Guest 指令时,必须在同一处 提交它的 T0 和 T1 实现;缺少任意一侧时构建失败。CPU profile 只能启用 T0/T1 coverage 完整的扩展。
三个执行层共用执行循环、异常处理和内存系统。T0 不另写 IRQ、SMC 或 MMIO 逻辑,T1 和 T2 也使用同一种状态恢复格式。
性能数据按层级拆开。T0 测一次性代码,T1 测基线 JIT,T2 测热点 Region。 cold、warm 和 hot 结果不混算。
1.1 为什么需要 T0、T1 和 T2
DBT 的总成本可以粗略写成:
C_total = C_decode + C_profile + C_compile + N * C_execute + C_invalidate
N 是同一段 Guest 代码的执行次数。冷代码的 N 很小,编译成本占比最高;
热点代码的 N 很大,单次执行成本会被重复放大。只使用一种执行方式,无法
同时处理这两种情况。
一次晋升要有收益,至少应满足:
N_remaining * (E_current - E_next)
> C_profile + C_compile + C_switch
运行时不知道 N_remaining,只能用执行计数、分支稳定度、编译预算和晋升
迟滞来近似。这里的阈值必须来自实测,不能把 T0、T1 或 T2 设成无条件路径。
- T0 解释已经解码的 Guest 指令,不生成 Host code。它省掉 IR 构造、优化、 寄存器分配和代码发布,适合首次执行、异常路径和很快退出的代码。T1/T2 尚未完成、编译失败或代码失效时也可以回到 T0;GDB single-step 也使用 T0 保持精确指令边界。代价是每条指令都要经过解释 dispatch,因此重复 执行到阈值后要晋升到 T1。
- T1 根据 T0 的重复执行计数生成基线 TB。它的目标是尽快得到可运行的 Host 指令,只做常量折叠、局部 DCE、简单寄存器分配和直接 lowering 等低成本 优化。T1 同时记录 entry、branch、backedge、indirect target、helper 和 MMU slow-path profile,供 T2 判断哪些路径值得继续编译。
- T2 只处理 T1 已经证明会长时间运行、分支方向相对稳定的 Region。T2 在 后台执行跨 TB 寄存器保留、bounded SSA、跨块 CSE/DCE、访存 proof 复用 等成本较高的优化。编译期间仍运行 T1;编译失败、超预算或失效时也回到 T1,不阻塞 Guest。
运行时是一条带晋升和回退的执行链。每个 TranslationSlot 同一时刻只选择
一个 active entry。晋升顺序固定为
T0 -> T1 -> T2,不能由 T0 直接进入 T2;回退顺序相反。这样,短命代码
不占 JIT 和 code-cache 预算,中等热度代码停在 T1,只有能摊销编译成本的
代码进入 T2。profile 和编译预算决定晋升,阈值由第 8 节的 break-even
数据确定。
1.2 为什么 T0 和 T1 必须成对实现
T0 和 T1 执行同一套 ISA 语义,但工作方式不同:exec_t0 直接修改 CPU
state,emit_t1 生成稍后执行的 IR。把两者写成一个泛型函数会掩盖状态
提交、异常和延迟写回的差异;把它们放在不同目录独立维护,又容易出现某条
指令只支持一个层级。方案因此规定:同一指令的两个函数放在同一个 module
中,保持独立实现,并作为一个交付单元注册。
这里的“同步”指语义同步,不是要求两段代码逐行相同。两侧至少要一致处理:
- PC 推进、x0、符号扩展、XLEN 和非法 encoding;
- memory fault、异常优先级、访存宽度以及副作用提交顺序;
- privilege、CSR、PMP、atomic reservation 和架构状态更新;
- FP rounding、NaN boxing、
fflags、FS dirty,以及后续向量指令的VL/VTYPE、mask、tail、vstart、saturation 和部分提交顺序。
FPU 是这种约束的直接例子。T0 可以调用 softfloat/reference helper;T1
可以在 rounding mode、NaN 和 flag 条件满足时使用 Host SSE/FMA,否则回到
同一个 helper。两个路径允许采用不同实现,但最终 CPU state 和异常必须
相同。以后加入 VPU/RVV 时也沿用这个方式:T0 提供逐元素参考语义,T1 在
VL/VTYPE、mask、tail policy 和部分提交顺序可证明时使用 Host SIMD,
未命中的情况走参考 helper。RVV Host SIMD 不属于第一版范围,这里只固定
实现约束。
这种组织方式增加了一个 T0 函数,但减少了长期维护成本:生成器在构建时 检查实现是否成对存在,T0/T1 differential test 检查行为是否漂移,调试时 还可以把 T0 当作同进程参考路径。立即数归一化、合法性判断和 softfloat 等基础语义放在共享 helper 中;T1 专有的 lowering 不反向污染 T0。
T2 不要求每条指令再提供一个 emit_t2。它只消费已经通过 effect verifier
的 T1 typed IR;不能安全优化的指令会结束 Region。这使 ISA coverage 的
完成条件保持为 T0+T1,T2 coverage 只影响性能。
1.3 代价和限制
三层执行会增加 metadata、profile counter、编译队列和失效协议。T0/T1 成对实现也比单一 translator 多一个维护入口。对应的限制是:counter 使用 饱和计数并在晋升后退出热路径;T2 有指令数、side exit 和代码大小预算; 所有层级通过同一个 slot/version/epoch 协议发布和失效;共享 helper 只 承载确实相同的语义。
这些成本是否值得,由 T0 冷态、T1 预热态和 T2 热态三组数据分别判断。 某个层级没有达到第 8 节的门槛时,不用其他层级的收益抵消。三级结构只 提供控制编译投入的机制,不直接证明 2–3 倍性能;最终结论以自动分层的 端到端结果为准。
以下内容不在第一版范围内:
- persistent code cache;
- 外部编译进程;
- syscall 或 libc HLE;
- RVV Host SIMD lowering;
- 在 RISC-V 单核路径稳定前推广到其他 Guest 架构。
2. 当前代码与改造边界
RISC-V decoder 由 insn32.decode、insn16.decode 和 xthead.decode
生成。生成器当前输出
Decode trait,并为每条指令生成一个带默认实现的 trans_* 方法。默认实现
返回 false。RISC-V build script
把三个 decode 文件编译进 OUT_DIR,真正的 T1 实现在
guest/riscv/src/riscv/trans/mod.rs 中。
这套结构有两个问题:
- decoder 直接回调 translator,解码结果不能被 T0 复用;
trans_*有默认实现,新增 pattern 后即使漏写实现也可能继续编译。
改造后,decoder 只负责把原始指令变成带类型的 DecodedInsn。T0 和 T1
分别消费这个结果。现有 gen_rvi.rs、gen_rva.rs 等文件保留为 T1 的 IR
构造辅助函数,不再作为指令注册入口。
建议的目录结构如下:
guest/riscv/src/riscv/
├── insn/
│ ├── mod.rs
│ ├── rvi.rs
│ ├── rvm.rs
│ ├── rva.rs
│ ├── rvf.rs
│ ├── rvd.rs
│ ├── privileged.rs
│ └── xthead.rs
├── tier/
│ ├── mod.rs
│ ├── t0.rs
│ └── t1.rs
├── trans/
│ ├── gen_common.rs
│ ├── gen_rvi.rs
│ └── ...
└── insn_decode.rs
insn/*.rs 保存每条指令的 T0/T1 函数。文件仍按 ISA extension 分组,避免
为数百条指令创建数百个小文件;同一条指令的两个函数必须相邻。
3. 指令实现模型
3.1 语义实现与编码描述
语义实现和二进制编码分开登记。一个语义可以对应多个 encoding,例如
addi 和 c.addi;feature、XLEN 和静态合法性属于 encoding,不能挂在
归一化后的语义上。
下面的代码只是拟议接口,名称可以在实现时调整:
pub struct InsnMeta {
pub id: InsnId,
pub name: &'static str,
pub effects: InsnEffects,
}
pub struct InsnImpl<A> {
pub meta: &'static InsnMeta,
pub exec_t0: fn(&mut T0Context<'_>, &A) -> StepResult,
pub emit_t1: fn(&mut T1Context<'_>, &A) -> EmitResult,
}
pub struct EncodingSpec {
pub id: EncodingId,
pub semantic: InsnId,
pub required_features: FeatureSet,
pub xlen: XlenMask,
pub static_guard: StaticGuardId,
}
以 addi 为例,代码放在同一个 module 中:
pub(crate) mod addi {
use super::*;
pub const META: InsnMeta = InsnMeta {
id: InsnId::Addi,
name: "addi",
effects: InsnEffects::PURE,
};
pub const IMPL: InsnImpl<ArgsI> = InsnImpl {
meta: &META,
exec_t0,
emit_t1,
};
fn exec_t0(ctx: &mut T0Context<'_>, a: &ArgsI) -> StepResult {
let value = ctx.read_x(a.rs1).wrapping_add(a.imm as u64);
ctx.write_x(a.rd, value);
StepResult::Next
}
fn emit_t1(ctx: &mut T1Context<'_>, a: &ArgsI) -> EmitResult {
let src = ctx.gpr_or_zero(a.rs1);
let imm = ctx.ir.new_const(Type::I64, a.imm as u64);
let dst = ctx.ir.new_temp(Type::I64);
ctx.ir.gen_add(Type::I64, dst, src, imm);
ctx.set_gpr(a.rd, dst);
EmitResult::Continue
}
}
InsnImpl<ArgsI>、InsnImpl<ArgsR> 等类型不放进同一个 slice。生成器用
DecodedInsn 的穷尽 match 保留 typed dispatch,只把非泛型的
InsnMeta 放进 ALL_INSN_META。
这里保留两个独立函数。T0 修改真实 CPU state,T1 生成 IR。两者不能共用
一段泛型代码来掩盖语义差异,但应复用立即数处理、CSR 合法性、FP rounding
和内存访问等已有 helper。InsnEffects 至少描述:
bitflags! {
pub struct InsnEffects: u32 {
const READ_MEM = 1 << 0;
const WRITE_MEM = 1 << 1;
const MAY_FAULT = 1 << 2;
const CONTROL = 1 << 3;
const CSR = 1 << 4;
const FP_STATE = 1 << 5;
const ATOMIC = 1 << 6;
const PRIVILEGED = 1 << 7;
}
}
手写 effect 只作为保守上界,默认值是 CONTROL | MAY_FAULT | READ_MEM | WRITE_MEM | CSR | FP_STATE | ATOMIC | PRIVILEGED。T1 verifier 从实际 IR op
和 helper registry 计算 effect;实际 effect 超出声明时拒绝该次翻译,
回退 T0,并让 debug/CI 测试失败。T2 只使用 verifier 产生的
VerifiedEffects 判断 Region 边界。T0 的状态提交由 T0Context 和
StepResult 控制,不依赖这份元数据。
3.2 decoder 生成物
guest/riscv/isa/manifest.toml 登记 semantic id、参数类型和 Rust module。
.decode 中的每个 pattern 指定 semantic id、required features、XLEN 和
static guard。machina-decode 同时读取两者,不再生成带默认方法的
Decode trait,而是生成:
pub struct Decoded {
pub insn: DecodedInsn,
pub raw: u32,
pub len: u8,
}
pub enum DecodedInsn {
Addi(ArgsI),
Add(ArgsR),
Lw(ArgsI),
// ...
}
pub fn decode32(
cfg: &RiscvCfg,
insn: u32,
) -> Result<Decoded, DecodeError>;
pub fn decode16(
cfg: &RiscvCfg,
insn: u16,
) -> Result<Decoded, DecodeError>;
压缩指令如果与 32 位指令语义相同,直接归一化到同一个 variant。例如
c.addi 的 EncodingSpec 要求 C extension 并保存自己的 rd/imm/XLEN
guard,成功后返回 DecodedInsn::Addi;指令长度放在 Decoded.len。
确实具有不同合法性或副作用的压缩指令使用独立 variant。动态 CSR/PMP/FP
合法性由 T0/T1 共用的架构 helper 检查。
生成器还输出两个穷尽匹配的 dispatcher:
impl DecodedInsn {
pub fn exec_t0(
&self,
ctx: &mut T0Context<'_>,
) -> StepResult;
pub fn emit_t1(
&self,
ctx: &mut T1Context<'_>,
) -> EmitResult;
}
每个 match arm 都引用 registry 指定 module 的 IMPL.exec_t0 和
IMPL.emit_t1。.decode 中新增了 pattern,但 registry、module、IMPL
或任意一个函数缺失时,cargo check 直接失败。现有“默认返回 false”的
路径不再用于已声明支持的 RISC-V 指令。
生成器同时输出同构的 ALL_INSN_META 和 ALL_ENCODING_META。CPU profile
只有在某个 extension 下的全部 pattern 都有完整实现时,才能把该 extension
标为可用。
make check-isa-coverage 检查以下条件:
- 每个 decode pattern 恰好映射到一个 semantic id;
- 每个 semantic id 至少被一个 pattern 使用,或显式标记为 internal;
- 生成的 typed dispatcher 能同时引用 T0/T1 函数;
- 16 位 alias 使用自己的 feature、XLEN 和 static guard;
- 所有手写 decoder 都已迁移到
.decode,或作为 manual encoding 登记; - RISC-V frontend 只能调用生成的
decode16/decode32,不能从 decoder 直接调用 T0/T1 实现; - CPU profile 宣布的 extension 没有缺项。
当前 gen_xthead::decode_xthead 中的手写 vendor encoding 必须在 P2 前
迁移或登记;只检查 xthead.decode 中已有的 pattern 不算 coverage 完整。
上述条件由 cargo check 和 make check-isa-coverage 执行。
3.3 T0/T1 上下文
T0 不直接散落访问 RiscvCpu 字段。T0Context 提供安全接口:
pub struct T0Context<'a> {
pub cpu: &'a mut RiscvCpu,
pub pc: u64,
pub insn_len: u8,
pub memory: &'a mut dyn InterpMemory,
}
pub enum StepResult {
Next,
Jump(u64),
Exit(ArchExit),
Fault(MemFault),
}
InterpMemory 复用当前 SoftMMU、PMP、MMIO 和 SMC write barrier。T0 load
不能直接解引用 Guest 地址,也不能另写一份地址翻译。CSR、FP 和 atomic
同样通过已有的架构 helper,避免 T0/T1 在边界条件上分叉。
T1Context 包装当前 RiscvDisasContext 和 IR Context。迁移期间可以
保留现有 gen_* helper;原来的 trans_addi 等方法逐条搬到
insn/*.rs 的 emit_t1。
3.4 新增一条指令的流程
以后补指令按固定顺序提交:
- 新语义先在 manifest 登记,再在
.decode中加入 encoding、feature、 XLEN 和 static guard; - 在对应 extension 文件中紧挨着实现
exec_t0和emit_t1; - 定义保守的
InsnEffects,补充 legality、共享 helper 和专有 lowering 所需的 guard/fallback; - 在
machina-tests中添加 encoding、非法 encoding、extension disabled、T0 结果、T0/T1 differential 和 QEMU differential; - 运行
make check-isa-coverage、目标 ISA 测试和 full-system smoke。
同一次语义修改必须同时检查两个函数。检查内容包括操作数归一化、PC 和 退休计数、异常、memory/CSR/FP/atomic 副作用,以及所有专有 fast path 的 fallback。FPU 或后续 VPU/RVV lowering 即使只优化 T1,也必须有 T0 参考 结果和 T0/T1 differential case。
只写 T1 或只写 T0 的提交过不了编译。某个 extension 仍在迁移时,可以保留 legacy T1-only CPU profile,但新的 tiered profile 不得对外声明该 extension。 默认启用 tiered execution 之前,RV64GC 中所有已公开指令都必须完成成对迁移。
3.5 T2 与指令实现的关系
新增 Guest 指令不要求再写一个 emit_t2。T2 消费 T1 产生的 typed IR。
如果一条指令只生成通用 helper call,或者 VerifiedEffects 表示它不能
安全进入 Region,T2 在该指令前结束 Region,继续执行对应 T1 TB。
因此,ISA 支持的最小原子单元是 T0+T1;T2 coverage 是独立的优化指标,不 影响指令是否被 Machina 支持。
4. 运行时结构
4.1 TranslationSlot
JumpCache 不再直接保存容易失效的 TB index,而是保存稳定 slot id。
pub struct TranslationSlot {
key: TranslationKey,
dispatch: AtomicPtr<DispatchState>,
control: Mutex<SlotControl>,
entry_count: SaturatingCounter,
branch_profile: BranchProfile,
indirect_profile: IndirectProfile,
}
pub struct DispatchState {
version: u64,
active_tier: Tier,
entry: *const Entry,
}
pub struct SlotControl {
version: u64,
invalid: bool,
source: Option<SourceIdentity>,
tiers: [Option<EntryRef>; 3],
jobs: [JobState; 3],
}
active tier、可用 entry 和后台 job 状态是三组正交状态,不能合并成一个
CompileState enum。例如 T2 正在编译时 T1 仍然 active;策略性降级到 T1
时,T2 entry 也可能继续保留。SlotControl 只在 promotion、publish、
invalidation 和回收时加锁,不在 entry fast path 上加锁。
TranslationKey 是不随 source invalidation 改变的 lookup key,至少包含
Guest PC、ISA/CPU profile、TB flags、privilege/mmu-index、稳定的
address-space id、Host feature 和 helper ABI 版本。VA->PA 映射、权限和
page generation 不放在 key 中,而是属于每个 version 的
SourceIdentity:
pub struct SourceIdentity {
pages: SmallVec<[SourcePage; 2]>,
}
pub struct SourcePage {
guest_range: Range<u64>,
phys_page: u64,
mapping_generation: u64,
code_generation: u64,
executable: bool,
}
source page 集合不能假定只有一页:一条 32 位指令本身就可能跨页,T1 TB
也可能覆盖多页。T0/T1/T2 安装前都要验证完整集合。satp/ASID 被复用但
映射变化时,slot 可以保留,新的 SourceIdentity 随 version 发布。
dispatch 指向不可变的 DispatchState。T0 entry 是解释 trampoline,
T1/T2 entry 是 Host code。dispatcher 进入 epoch 后用 Acquire load 读取
一次 dispatch,并验证 entry version;LinkCell 也只缓存 slot,不缓存裸
Host code 地址。
publish 和 invalidation 共用 SlotControl 锁。T0 decode、T1 compile 和
T2 compile 都持有
BuildToken { kind, version, serial, source_identity };耗时工作在锁外
完成,提交时重新加锁,并同时验证 token、version、job state 和全部 source
page generation。验证通过后先更新 source/tiers,再构造新的
DispatchState,最后用 Release swap 更新 dispatch。校验失败的 build
不能写 dispatch,其 decode/code 经 epoch 回收。这样连续两次 invalidation
也不能让旧 decode 或旧 code 覆盖新的 trampoline。
4.2 T0
T0 首版一次执行一条 Guest 指令。这样异常 PC、instret、IRQ、GDB 和
跨页取指的边界最清楚。每个 slot 缓存 DecodedInsn 和原始 source
generation,命中后不重复 decode。
单指令版本稳定后,再根据数据决定是否缓存 4/8 条 DecodedBlock。这一步
只能减少 dispatch,不能改变每条指令的可观察边界。
T0 不另建主循环。现有 cpu_exec_loop 中 TB 选择和执行部分被抽成
run_entry,T0/T1/T2 都返回同一个结果:
pub struct ExecOutcome {
pub tier: Tier,
pub exit: ExitKind,
pub guest_bytes: u32,
pub retired_insns: u32,
pub fault_site: Option<FaultSiteId>,
}
当前 exec loop 的 SMC、memory fault 优先级、arch exit、IRQ、GDB、WFI 和 monitor 处理继续共用一条后处理路径。
4.3 T1
达到 T0 hit 阈值后,在 SlotControl 中为当前 version 创建唯一 T1
BuildToken。第一版可同步编译,状态机稳定后再放入 per-vCPU 编译队列。
编译期间 active 仍指向 T0;失败或队列满也继续 T0。T1 按 4.1 节的共同
提交路径发布,不能在锁外先检查 version、再单独写 active entry。
T1 entry 使用 canonical ABI;跨 TB 的非 canonical 寄存器布局需要版本化 edge contract 和 reconciliation stub。
T1 的编译预算以 Guest 指令数、IR op 数和 wall time 三项限制。超预算时不 生成半成品 TB,slot 保持 T0。
4.4 T2
只有 T1 记录 T2 所需的 entry、branch、backedge 和 indirect-target
profile。达到热度并且 profile 稳定后,在 SlotControl 中创建 T2
BuildToken。后台 worker 读取不可变 TranslationCapsule,不能读取 live
CPU、MMU 或 RAM。
T2 v1 使用单 hot path、有限 side exit 和最多一个 loop backedge:
- 不超过 256 条 Guest 指令;
- 不超过 8 个 side exit;
- Host code 不超过 32 KiB;
- 首版只跨一个 source physical page;
- 不跨 MMIO、特权指令或不能证明安全的 atomic sequence。
T2 使用带 Memory/CSR/Atomic token 的 bounded SSA。每个 fault、guard、
safepoint 和 side exit 都有 StateVersion,记录 Guest PC、GPR/FPR/CSR、
FLAGS、FP flags、LR/SC reservation、退休偏移和副作用提交状态。
T2 通过 4.1 节的共同提交路径验证全部 source generation、version 和 ABI。 side exit 优先回对应的 T1 slot。目标没有有效 T1 时才回 T0。
4.5 降级、失效和回收
代码缓存压力先在 SlotControl 中选择仍可用的较低层 entry,再交换
dispatch;source version 不变。旧 entry 要等 epoch grace 后才能回收。
降级到 T1 不要求立即删除 T2,是否删除由 code-cache policy 决定。
SMC、FENCE.I、映射/权限变化或 PMP 更新会使同一 source version 下的
T0/T1/T2 一起失效:
- 加
SlotControl锁,bump version、标记 invalid 并取消全部旧 build; - 在同一临界区把
dispatch换成新 version 的 invalidation trampoline; - 清除 LinkCell、PIC 和入边;
- 撤销各 vCPU 缓存的“非代码页”负证明;
- 为新 version 创建 T0
BuildToken,在锁外 fetch/decode; - 重新加锁验证 token 和完整
SourceIdentity,再安装 T0; - 等待 epoch grace,再回收旧 decode、代码和 state map。
CPU store、helper、LR/SC/AMO、DMA、debugger 和 loader 都必须经过统一的 code-page write barrier。
5. T1/T2 后端优化
以下优化项只有在对应计数器或 PMU 数据确认成本后才实施。
5.1 状态同步与 FaultSite
当前 ExitTb/GotoTb 和通用 helper call 会同步 globals、spill caller-saved,
并使寄存器缓存失效。每条 InsnStart 还可能生成一次 fault_pc 写回。
T1 需要 helper effect 描述:
pure | reads_env | writes_state | may_fault | may_exit
只同步 helper 实际读取或修改的状态。fault_pc 改为 Host PC 到 Guest PC 的
side table,只在 may-fault/may-exit 点生成记录。T2 复用同一 FaultSite
格式。
5.2 SoftMMU
当前 x86-64 load
和 store
hit path 会保存和恢复多个 scratch register,计算 TLB index 后使用 imul
定位 entry;每次 store 还写 dirty byte。
改造项包括:
- T1 allocator 知道 SoftMMU fast path 的 clobber;
- 保留一个 TLB base register;
- entry stride 用 shift/LEA;
- miss/MMIO/fault 进入共享的 out-of-line stub;
- T2 在同页地址和 permission generation 不变时复用 data proof;
- 普通 data page 不写 dirty byte,code-page 身份由可失效负 proof 保证。
5.3 分支和链接
JALR 当前返回 Rust dispatcher。每个间接跳转点增加 2--4 项 PIC,命中后 加载 stable slot 的 active entry。直接边使用 LinkCell,不把裸 Host code 地址永久写进 Guest TB。
5.4 T1 codegen
T1 先做编译成本低的改动:next-use/linear-scan regalloc、立即数和 memory operand constraint、LEA folding、FLAGS reuse、lazy sign extension 以及 局部 CSE/DCE。
DIV/REM 直接 lower 到 Host 指令,显式处理除零和 INT_MIN / -1。FPR 纳入
可缓存状态;FS check 在 TB 内提升。无异常 FP 位操作先内联,普通 F/D 运算
在 rounding、NaN boxing 和 fflags 可证明时使用 SSE2/FMA,否则回
softfloat。
AMO 和 LR/SC 分开处理。自然对齐的普通 RAM AMO 可用 Host atomic。LR/SC 使用受限 Region,或者维护所有 CPU/DMA 写都会更新的 reservation-granule version,避免 ABA。RVWMO litmus 通过前不删除现有全局锁。
6. 实施顺序
P5 的进入条件是 P3 已提供 slot/version/epoch,P4 已提供 FaultSite 和 helper effect。阶段顺序由依赖关系决定,不按预估收益排序。
| 阶段 | 为什么在这里做 | 交付内容 | 退出条件 |
|---|---|---|---|
| P0 | 先把指令成对、effect 和 tier 统计变成机器可检查的约束,否则迁移中无法判断 coverage 和收益 | ISA manifest、DecodedInsn、InsnMeta/InsnImpl/EncodingSpec、T0/T1 成对生成检查、effect verifier、统一 ExecOutcome、强制 tier 模式和统计 |
RV64I 样例能够编译;缺任一实现时构建失败 |
| P1 | 用语义面较小的 RV64I 验证 T0/T1 接口、异常路径和差分测试,避免一开始迁移全部扩展 | 单指令 T0、RV64I 成对迁移、同步 T1 fallback | RV64I 的 T0/T1/QEMU differential 全通过 |
| P2 | tiered CPU profile 对外可用前补齐现有 ISA,避免不同层级宣称不同的指令集 | RV64GC、privileged、RVC 和 vendor 指令成对迁移;移除未登记的手写 decoder;tiered CPU profile | 已公开 ISA 无 T0/T1 coverage 缺口 |
| P3 | 异步编译和 T2 都依赖稳定的代码身份、发布和回收协议;先解决 stale entry,再增加优化层 | TranslationSlot、page version、T0->T1、LinkCell、epoch;先同步后异步 T1 |
SMC/GDB/IRQ 压力测试无 stale entry |
| P4 | T1 承担大部分常用代码,先去掉 helper、SoftMMU 和 regalloc 的固定税,得到可信的 T2 profile 与基线 | T1 helper effect、FaultSite、SoftMMU、PIC、regalloc 和 native lowering | warm suite 相对 QEMU 达到阶段门槛 |
| P5 | 只有 profile、state map 和失效协议稳定后,跨 TB 优化才能安全发布和回退 | T1 profile、TranslationCapsule、T2 queue、StateVersion 和 bounded Region |
hot suite 达到最终门槛 |
| P6 | 单核执行与失效语义稳定后再拆全局锁和扩展 SMP,便于区分 DBT 回归与并发问题 | per-vCPU staging、分片 L1/code cache、RISC-V SMP | 1/2/4 vCPU 正确性与扩展性通过 |
每个阶段保留独立 feature flag,并输出自己的时间、代码大小和 tier 计数。 阶段门槛用于决定改动是否保留;correctness 未通过时不能进入后续阶段。
7. 测试方案
所有测试继续放在 machina-tests。
7.1 指令级测试
生成器为每个 InsnMeta 输出稳定 InsnId。测试表按 InsnId 驱动,至少
覆盖:
- 一个正常 encoding;
- 固定位翻转后的非法 encoding;
- extension 关闭;
- x0、符号扩展、XLEN 和边界立即数;
- T0 执行结果;
- T0/T1 执行后的 PC、GPR、FPR、CSR、memory、exception 和
instret对比; - 与 QEMU 的 differential。
load/store、CSR、FP 和 atomic 还要覆盖 fault 与副作用顺序。任何一条指令 缺少 T0/T1 差分测试用例,coverage 检查失败。
7.2 Tier 切换测试
运行模式至少包括:
t0-only
t1-only
auto-no-t2
auto
random-tier-switch
测试主动在 promotion、编译完成、side exit、IRQ、SMC 和 code cache 回收 点切层。每次切层后比较完整架构状态。GDB single-step 强制 T0;T0 尚未覆盖 的迁移阶段,沿用当前单指令 ephemeral T1,但 tiered CPU profile 不能进入 这种状态。
7.3 系统测试
Linux boot、rCore ch1--8、MMIO、timer、virtio、跨页取指、PMP、页表切换、
SFENCE.VMA、FENCE.I、SMC、LR/SC 和 FP flags 都要在 T0-only、
T1-only 和 auto 下运行。oracle 或 QEMU 缺失时测试失败,不允许静默 skip。
8. 性能测量
QEMU 基线固定为同一台 Linux x86-64 裸机上的
qemu-system-riscv64 -accel tcg,thread=single。两端使用相同 Guest、
ISA、RAM、固件和可见设备。完整命令、commit、编译器、Host 配置和二进制
哈希写入 tools/perf/bench.lock.toml。
计时使用 Host monotonic clock 和固定 Guest 工作量,不使用 Guest
cycle/time。每个用例有 checksum。正式结果至少 30 个随机 ABBA 配对
样本,并报告 median、MAD 和 paired-bootstrap 95% CI。
8.1 基准清单和样本生命周期
P0 在 tools/perf/manifest.toml 中冻结基准清单。每个用例必须记录:
- 用例 id、负载组、命令和输入文件哈希;
- 角色:
tuning(调参)、holdout(验证)或guardrail(护栏); mandatory = true/false、权重和最低执行次数;scorecards = [t0-cold, t1-warm, t2-steady, auto-e2e]的适用子集;- 自动分层扫描点和
auto-e2e的固定验收迭代点; - Guest 指令构成、预期 tier 占比和结果 checksum;
- 预热条件、计时区间、超时和异常样本规则。
计入主性能结果的负载组至少包含:
- integer/FP compute;
- direct/indirect branch 和 call/return;
- SoftMMU hit 下的 load/store 和 mixed-width memory;
- Guest Linux 中以 CPU 为主的应用负载。
Linux boot、设备 I/O、MMIO、SMP 和 code-cache 压力作为护栏用例单独 报告,不计入 2–3 倍的聚合结果。每个计分负载组至少有一个验证用例,任何 负载组的总权重不得超过 40%。阈值只能在调参用例上调整;验证用例的输入、 权重和角色在首次调优前锁定。
Machina 和 QEMU 的每个 ABBA 样本都从新进程开始,并在同一个进程内完成 相同次数的预热和计时。两端使用相同的 CPU affinity、频率策略、Guest 镜像 和输入。预热不跨样本复用,Host page cache 策略写入 lockfile。
正式 QEMU 基线必须通过 trace 或 plugin 输出新 TB 计数;缺少该计数时不能
运行预热态验收。调参阶段分别观察 Machina 的 tier/queue 统计和 QEMU 的
新 TB 计数。每个用例的正式预热轮数取两端所需轮数的较大值,并在 lockfile
中冻结。t1-warm 和 t2-steady 的正式样本两端都执行该固定轮数;
Machina 未达到预期 tier 占比、编译队列未清空,或任一端在计时区间生成新
TB 时,该次样本无效。t0-cold 和 auto-e2e 不使用这条预热规则,分别按
8.2 节和 8.3 节从冷态计时。
先在用例内计算配对加速比分布,再按 manifest 权重求负载组的几何平均; 负载组之间等权。95% CI 使用固定 seed 的分层 paired bootstrap,在每个 用例内重采样 ABBA 样本,脚本、原始样本和聚合结果一并提交。
8.2 T0、T1、T2 分开验收
T0 冷态、T1 预热态和 T2 热态使用三组独立结果。
T0 用例使用从未执行过的新代码区,固定 64、1K、16K 个 unique basic
block,每块只执行 1--2 次。计时不包含公共 Guest 启动和 UART。有效样本要求
T0 指令占比 >= 99.9% 且没有 JIT。冷 T1 使用相同的新代码区和 t1-only
模式,计时包含 decode、IR、codegen 和 publish。T0 相对冷 T1 的单次执行
几何平均加速比 95% CI 下界应达到 1.10×。
M0_BASE_SHA 指 P0 开始前冻结的 Machina commit;对应构建命令和冷态命令
写入 lockfile。自动分层的冷态回退门槛为:
upper95(T_auto / T_M0) <= 1.05
T1 用例先运行到固定最小预热轮数,且连续两轮 new_t1_tb = 0,随后只重置
计时和统计,不清 code cache。有效样本要求 T1 指令占比 >= 99%、T2 为零,
计时区间没有新 TB。预热态负载组相对预热态 QEMU 的几何平均
95% CI 下界应达到 1.0×,总体点估计以 1.5× 为阶段目标。
T2 用例在 promotion 和后台编译全部完成后开始计时。有效样本要求 T2 指令
占比 >= 95%,计时区间没有编译任务。每个计分负载组相对预热态 QEMU
的几何平均 95% CI 下界均需达到 2.0×,总体点估计达到
2.5×,任一必选用例的 CI 下界不得低于 1.5×。
还要单独报告 T2 相对 T1-only 的收益:
S12 = T_t1-only / T_t2-steady
S12 的负载组几何平均 95% CI 下界必须达到 1.25×。
8.3 自动分层
从新代码第一次进入开始,扫描迭代数:
1, 2, 4, 16, 64, 256, 1K, 4K
这条曲线包含 profile、排队和编译成本。每个点同时测 t0-only、
t1-only、auto-no-t2 和 auto;强制模式也必须包含自身的 decode、
编译和 publish 成本。令:
T_best = min(T_t0-only, T_t1-only, T_auto-no-t2)
必选验证用例需要满足:
upper95(T_auto / T_best) <= 1.10
T2 的 break-even 定义为第一个满足
upper95(T_auto / T_auto-no-t2) <= 0.95,并在所有更大预注册迭代点保持该
条件的点。promotion 阈值只在调参用例上选择,再用验证用例验证。
对外性能结论必须基于自动分层的端到端热点测试集,profile 和编译成本计入
总时间;每个计分负载组的 95% CI 下界必须达到 2.0×。仅
t2-steady 达标时,结论范围限定为“T2 热态达到 2–3 倍”。
每个样本必须输出:
guest_insns_t0/t1/t2;- T1/T2 编译数量、时间和队列等待;
- promotion、demotion、stale job 和 cancel;
- side exit、deopt 和 fallback;
- code size、TLB、helper、spill、state sync、PIC hit/miss;
- Host cycles、instructions、branches 和 cache misses。
9. 完成条件
关闭本 issue 前,需要同时满足三项条件:
- ISA:decoder 已改成
DecodedInsn + InsnMeta/InsnImpl/EncodingSpec;RV64GC、RVC、 privileged 和当前公开 vendor extension 完成成对迁移;缺 T0 或 T1 实现时构建失败。 - 正确性:T0-only、T1-only、auto-no-t2 和 auto 能运行完整系统; promotion、fallback、SMC、IRQ、GDB 和 code-cache 回收测试通过。
- 性能:原始数据、脚本和 lockfile 可复现,并通过 8.2 节和 8.3 节的 全部门槛。
新增指令的成对实现流程和 coverage gate 同步写入贡献文档。
参考
当前源码诊断基于 Machina
4c902e94f0f82f55ba24d33ee13c5bdd03b73538。正式比较使用的 QEMU commit
由 P0 lockfile 固定。