"""Modeled RV64IMAC instruction-class cycle accounting for the native core. **Every number in this module is a modeled default - NOT a measurement.** The tables encode deliberately simple, public assumptions about an in-order single-issue RISC-V core: one cycle for plain ALU work, a multi-cycle cost for the M extension, a fixed modeled latency added when a memory access completes, and a larger one for MMIO. They are *not* derived from the GSP core, not from silicon, and not from vendor data. The simulator's fidelity statement is unchanged (docs/06-ACCURACY.md): instruction semantics (L0) and memory behaviour (L1) are modelled; L2 (time/concurrency) is **not**. A cycle count from this module is a deterministic accounting artifact under an explicit model - never a timing, latency, throughput or silicon claim. The model is deterministic by construction: cost depends only on the decoded instruction class (``sim.decode`` kind/sub) and whether a completed access was RAM or MMIO. No host clock, no entropy, no caches, no pipeline hazards. """ from __future__ import annotations from .decode import ( A_DIV, A_DIVU, A_MUL, A_MULH, A_MULHSU, A_MULHU, A_REM, A_REMU, K_AMO, K_AUIPC, K_BRANCH, K_CSR, K_EBREAK, K_ECALL, K_FENCE, K_ILLEGAL, K_JAL, K_JALR, K_LOAD, K_LUI, K_MRET, K_OP, K_OP32, K_OPI, K_OPI32, K_STORE, ) #: Model identifier written into session metadata and reports. CYCLE_MODEL_NAME = "rv64imac-modeled/1" #: Fidelity label that must travel with every cycle number from this module. CYCLE_FIDELITY = "L2-not-modelled; cycles are modeled defaults, not measured" #: One-line provenance/label, repeated wherever the model is described. NOTE = ( "modeled defaults sourced from public in-order RISC-V pipeline assumptions; " "not measured, not a silicon timing claim" ) # Instruction classes used for reporting and per-class reasoning. CLASS_ALU = "alu" CLASS_BRANCH = "branch" CLASS_JUMP = "jump" CLASS_MUL = "mul" CLASS_DIV = "div" CLASS_LOAD = "load" CLASS_STORE = "store" CLASS_AMO = "amo" CLASS_CSR = "csr" CLASS_TRAP = "trap" CLASS_FENCE = "fence" CLASS_ILLEGAL = "illegal" _CLASS_BY_KIND = { K_LUI: CLASS_ALU, K_AUIPC: CLASS_ALU, K_JAL: CLASS_JUMP, K_JALR: CLASS_JUMP, K_BRANCH: CLASS_BRANCH, K_LOAD: CLASS_LOAD, K_STORE: CLASS_STORE, K_OPI: CLASS_ALU, K_OPI32: CLASS_ALU, K_FENCE: CLASS_FENCE, K_CSR: CLASS_CSR, K_ECALL: CLASS_TRAP, K_EBREAK: CLASS_TRAP, K_MRET: CLASS_TRAP, K_AMO: CLASS_AMO, K_ILLEGAL: CLASS_ILLEGAL, } #: Sentinel in ``base_costs``: the cost lives in ``op_costs`` (M extension). _SENTINEL = -1 _M_EXT_KINDS = (K_OP, K_OP32) class CycleModel: """Table-driven, deterministic instruction-class cycle costs. All keyword values are **modeled defaults** (see the module docstring); they are parameters so a future calibration can replace them without touching the execution loop. ``memory_latency``/``mmio_latency`` are the extra cycles added when a load/store/AMO access completes (RAM vs modelled-MMIO respectively); they are *not* cache models. """ name = CYCLE_MODEL_NAME def __init__( self, *, alu: int = 1, # plain integer ALU / shift / lui / auipc / op-imm branch: int = 1, # conditional branch (taken penalty deliberately not modelled) jump: int = 1, # jal / jalr mul: int = 3, # mul / mulh / mulhsu / mulhu (conservative) div: int = 20, # div / divu / rem / remu (conservative, data-independent) load: int = 1, # load base; + memory_latency/mmio_latency on completion store: int = 1, # store base; + memory_latency/mmio_latency on completion amo: int = 2, # amoswap/lr/sc base; + one memory latency on completion csr: int = 2, # CSR read/modify/write trap: int = 3, # ecall / ebreak / mret fence: int = 1, # fence / fence.i (no ordering model; nominal) illegal: int = 1, # decode failure (execution stops immediately) memory_latency: int = 3, # extra cycles for a completed RAM access mmio_latency: int = 12, # extra cycles for a completed unmodelled-MMIO access ): for label, value in ( ("alu", alu), ("branch", branch), ("jump", jump), ("mul", mul), ("div", div), ("load", load), ("store", store), ("amo", amo), ("csr", csr), ("trap", trap), ("fence", fence), ("illegal", illegal), ("memory_latency", memory_latency), ("mmio_latency", mmio_latency), ): if not isinstance(value, int) or isinstance(value, bool) or value < 0: raise ValueError(f"cycle model: {label} must be a non-negative int, got {value!r}") if alu < 1 or branch < 1 or jump < 1: raise ValueError("cycle model: alu/branch/jump must be >= 1 for a clean per-instruction model") self.alu = alu self.branch = branch self.jump = jump self.mul = mul self.div = div self.load = load self.store = store self.amo = amo self.csr = csr self.trap = trap self.fence = fence self.illegal = illegal self.memory_latency = memory_latency self.mmio_latency = mmio_latency base = [0] * 18 base[K_LUI] = alu base[K_AUIPC] = alu base[K_JAL] = jump base[K_JALR] = jump base[K_BRANCH] = branch base[K_LOAD] = load base[K_STORE] = store base[K_OPI] = alu base[K_OP] = _SENTINEL base[K_OPI32] = alu base[K_OP32] = _SENTINEL base[K_FENCE] = fence base[K_CSR] = csr base[K_ECALL] = trap base[K_EBREAK] = trap base[K_MRET] = trap base[K_AMO] = amo base[K_ILLEGAL] = illegal op = [alu] * 18 for sub in (A_MUL, A_MULH, A_MULHSU, A_MULHU): op[sub] = mul for sub in (A_DIV, A_DIVU, A_REM, A_REMU): op[sub] = div # Immutable fast-path tables consumed by Machine.run's hot loop. self.base_costs = tuple(base) self.op_costs = tuple(op) # -- queries ----------------------------------------------------------- def instruction_cost(self, kind: int, sub: int = 0) -> int: """Modeled cycles for one decoded instruction (memory latency excluded).""" cost = self.base_costs[kind] if cost == _SENTINEL: return self.op_costs[sub] return cost def memory_cost(self, is_mmio: bool = False) -> int: """Modeled extra cycles when a load/store/AMO access completes.""" return self.mmio_latency if is_mmio else self.memory_latency @staticmethod def classify(kind: int, sub: int = 0) -> str: """Human-readable class for a decoded instruction (reporting only).""" if kind in _M_EXT_KINDS: if sub >= A_DIV: return CLASS_DIV if sub >= A_MUL: return CLASS_MUL return CLASS_ALU return _CLASS_BY_KIND.get(kind, "other") def describe(self) -> dict: """The full table, for session metadata and reports.""" return { "name": self.name, "note": NOTE, "fidelity": CYCLE_FIDELITY, "classes": { CLASS_ALU: self.alu, CLASS_BRANCH: self.branch, CLASS_JUMP: self.jump, CLASS_MUL: self.mul, CLASS_DIV: self.div, CLASS_LOAD: self.load, CLASS_STORE: self.store, CLASS_AMO: self.amo, CLASS_CSR: self.csr, CLASS_TRAP: self.trap, CLASS_FENCE: self.fence, CLASS_ILLEGAL: self.illegal, }, "memory_latency": self.memory_latency, "mmio_latency": self.mmio_latency, } #: Shared default model. Machine() uses it unless told otherwise; it is #: immutable in practice, so one instance is safe for every target. DEFAULT_MODEL = CycleModel()