1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
|
"""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()
|