gevico/machina

[RFC] 优化 Machina DBT:T0 解释执行、T1 基线 JIT 与 T2 热点编译

Open

#162 opened on Jul 24, 2026

View on GitHub
 (0 comments) (0 reactions) (0 assignees)Rust (24 forks)github user discovery
RFCgood first issue

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.decodeinsn16.decodexthead.decode 生成。生成器当前输出 Decode trait,并为每条指令生成一个带默认实现的 trans_* 方法。默认实现 返回 falseRISC-V build script 把三个 decode 文件编译进 OUT_DIR,真正的 T1 实现在 guest/riscv/src/riscv/trans/mod.rs 中。

这套结构有两个问题:

  1. decoder 直接回调 translator,解码结果不能被 T0 复用;
  2. trans_* 有默认实现,新增 pattern 后即使漏写实现也可能继续编译。

改造后,decoder 只负责把原始指令变成带类型的 DecodedInsn。T0 和 T1 分别消费这个结果。现有 gen_rvi.rsgen_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,例如 addic.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 的状态提交由 T0ContextStepResult 控制,不依赖这份元数据。

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.addiEncodingSpec 要求 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_t0IMPL.emit_t1.decode 中新增了 pattern,但 registry、module、IMPL 或任意一个函数缺失时,cargo check 直接失败。现有“默认返回 false”的 路径不再用于已声明支持的 RISC-V 指令。

生成器同时输出同构的 ALL_INSN_METAALL_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 checkmake 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/*.rsemit_t1

3.4 新增一条指令的流程

以后补指令按固定顺序提交:

  1. 新语义先在 manifest 登记,再在 .decode 中加入 encoding、feature、 XLEN 和 static guard;
  2. 在对应 extension 文件中紧挨着实现 exec_t0emit_t1
  3. 定义保守的 InsnEffects,补充 legality、共享 helper 和专有 lowering 所需的 guard/fallback;
  4. machina-tests 中添加 encoding、非法 encoding、extension disabled、T0 结果、T0/T1 differential 和 QEMU differential;
  5. 运行 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 一起失效:

  1. SlotControl 锁,bump version、标记 invalid 并取消全部旧 build;
  2. 在同一临界区把 dispatch 换成新 version 的 invalidation trampoline;
  3. 清除 LinkCell、PIC 和入边;
  4. 撤销各 vCPU 缓存的“非代码页”负证明;
  5. 为新 version 创建 T0 BuildToken,在锁外 fetch/decode;
  6. 重新加锁验证 token 和完整 SourceIdentity,再安装 T0;
  7. 等待 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 loadstore 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、DecodedInsnInsnMeta/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.VMAFENCE.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;
  • 预热条件、计时区间、超时和异常样本规则。

计入主性能结果的负载组至少包含:

  1. integer/FP compute;
  2. direct/indirect branch 和 call/return;
  3. SoftMMU hit 下的 load/store 和 mixed-width memory;
  4. 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-warmt2-steady 的正式样本两端都执行该固定轮数; Machina 未达到预期 tier 占比、编译队列未清空,或任一端在计时区间生成新 TB 时,该次样本无效。t0-coldauto-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-onlyt1-onlyauto-no-t2auto;强制模式也必须包含自身的 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 前,需要同时满足三项条件:

  1. ISA:decoder 已改成 DecodedInsn + InsnMeta/InsnImpl/EncodingSpec;RV64GC、RVC、 privileged 和当前公开 vendor extension 完成成对迁移;缺 T0 或 T1 实现时构建失败。
  2. 正确性:T0-only、T1-only、auto-no-t2 和 auto 能运行完整系统; promotion、fallback、SMC、IRQ、GDB 和 code-cache 回收测试通过。
  3. 性能:原始数据、脚本和 lockfile 可复现,并通过 8.2 节和 8.3 节的 全部门槛。

新增指令的成对实现流程和 coverage gate 同步写入贡献文档。

参考

当前源码诊断基于 Machina 4c902e94f0f82f55ba24d33ee13c5bdd03b73538。正式比较使用的 QEMU commit 由 P0 lockfile 固定。

Contributor guide