summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/sim/cycles.py
blob: b841c6cd85d90e8223b6206a444429caebb0d787 (plain)
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()