blob: 5003f9d04a59605b3e83da45d3e22957b96cadfc [file] [log] [blame]
"""Classes shared by trace generators and TBMs."""
from __future__ import annotations
import collections
import dataclasses
from dataclasses import dataclass
import json
import re
from typing import Any, Dict, List, Optional, Sequence, Union
# Generated by `flatc`.
import FBInstruction.Instruction as FBInstr
@dataclass(slots=True, kw_only=True)
class Instruction:
"""Class representing an instruction.
This is an architecture-neutral representation of an instruction.
Architecture specific code involving RISC-V, Arm, x86, etc. should go in
trace generation and disassembly.
"""
addr: int
opcode: int
mnemonic: str
operands: Sequence[str]
inputs: Sequence[str]
outputs: Sequence[str]
is_nop: bool
is_branch: bool
branch_target: Optional[Sequence[int]]
is_flush: bool
is_vctrl: bool
loads: List[int]
stores: List[int]
lmul: Optional[Union[int, float]]
sew: Optional[int]
vl: Optional[int]
inputs_by_type_cache: Optional[Dict[str, Sequence[str]]] = None
outputs_by_type_cache: Optional[Dict[str, Sequence[str]]] = None
def __eq__(self, other) -> bool:
return id(self) == id(other)
def __hash__(self) -> int:
return id(self)
def to_json(self):
# For efficiency, and to match the FB:
inputs_by_type_cache = self.inputs_by_type_cache
self.inputs_by_type_cache = None
outputs_by_type_cache = self.outputs_by_type_cache
self.outputs_by_type_cache = None
res = json.dumps(dataclasses.asdict(self))
self.inputs_by_type_cache = inputs_by_type_cache
self.outputs_by_type_cache = outputs_by_type_cache
return res
@classmethod
def from_json(cls, s: str) -> Instruction:
"""Parse JSON string to Instruction."""
d = json.loads(s)
return cls(**d)
def fb_build(self, builder) -> Any:
mnemonic = builder.CreateString(self.mnemonic)
operands = [builder.CreateString(x) for x in self.operands]
FBInstr.StartOperandsVector(builder, len(operands))
for x in reversed(operands):
builder.PrependUOffsetTRelative(x)
operands = builder.EndVector()
inputs = [builder.CreateString(x) for x in self.inputs]
FBInstr.StartInputsVector(builder, len(inputs))
for x in reversed(inputs):
builder.PrependUOffsetTRelative(x)
inputs = builder.EndVector()
outputs = [builder.CreateString(x) for x in self.outputs]
FBInstr.StartOutputsVector(builder, len(outputs))
for x in reversed(outputs):
builder.PrependUOffsetTRelative(x)
outputs = builder.EndVector()
FBInstr.StartLoadsVector(builder, len(self.loads))
for l in reversed(self.loads):
builder.PrependUint64(l)
loads = builder.EndVector()
FBInstr.StartStoresVector(builder, len(self.stores))
for s in reversed(self.stores):
builder.PrependUint64(s)
stores = builder.EndVector()
FBInstr.Start(builder)
FBInstr.AddAddr(builder, self.addr)
FBInstr.AddOpcode(builder, self.opcode)
FBInstr.AddMnemonic(builder, mnemonic)
FBInstr.AddOperands(builder, operands)
FBInstr.AddInputs(builder, inputs)
FBInstr.AddOutputs(builder, outputs)
FBInstr.AddIsNop(builder, self.is_nop)
FBInstr.AddIsBranch(builder, self.is_branch)
FBInstr.AddBranchTarget(
builder, self.branch_target if self.branch_target else 0)
FBInstr.AddIsFlush(builder, self.is_flush)
FBInstr.AddIsVctrl(builder, self.is_vctrl)
FBInstr.AddLoads(builder, loads)
FBInstr.AddStores(builder, stores)
FBInstr.AddLmul(builder,
float(self.lmul) if self.lmul else 0.0)
FBInstr.AddSew(builder, self.sew if self.sew else 0)
FBInstr.AddVl(builder, self.vl if self.vl else -1)
return FBInstr.End(builder)
@classmethod
def from_fb(cls, buf) -> Instruction:
operands = ([
buf.Operands(i).decode("utf-8") for i in range(buf.OperandsLength())
] if not buf.OperandsIsNone() else [])
inputs = ([
buf.Inputs(i).decode("utf-8") for i in range(buf.InputsLength())
] if not buf.InputsIsNone() else [])
outputs = ([
buf.Outputs(i).decode("utf-8") for i in range(buf.OutputsLength())
] if not buf.OutputsIsNone() else [])
loads = ([buf.Loads(i) for i in range(buf.LoadsLength())]
if not buf.LoadsIsNone() else [])
stores = ([buf.Stores(i) for i in range(buf.StoresLength())]
if not buf.StoresIsNone() else [])
branch_target = buf.BranchTarget()
if branch_target == 0:
branch_target = None
lmul = buf.Lmul()
if lmul == 0.0:
lmul = None
elif lmul >= 1:
lmul = int(lmul)
return cls(addr=buf.Addr(),
opcode=buf.Opcode(),
mnemonic=buf.Mnemonic().decode("utf-8"),
operands=operands,
inputs=inputs,
outputs=outputs,
is_nop=buf.IsNop(),
is_branch=buf.IsBranch(),
branch_target=branch_target,
is_flush=buf.IsFlush(),
is_vctrl=buf.IsVctrl(),
loads=loads,
stores=stores,
lmul=lmul,
sew=buf.Sew(),
vl=buf.Vl())
def __str__(self) -> str:
return f"(0x{self.addr:x}) {self.mnemonic} {','.join(self.operands)}"
def max_emul(self) -> Union[int, float]:
"""Compute the biggest emul of all the vector registers for the
instruction.
Returns:
biggest emul.
"""
if self.lmul is not None:
if (self.mnemonic.startswith("vw") or
self.mnemonic.startswith("vfw") or
self.mnemonic.startswith("vn") or
self.mnemonic.startswith("vfn")):
return 2 * self.lmul
return self.lmul
return 1
def inputs_by_type(self) -> Dict[str, Sequence[str]]:
if self.inputs_by_type_cache is None:
self.inputs_by_type_cache = sort_regs_by_type(self.inputs)
return self.inputs_by_type_cache
def outputs_by_type(self) -> Dict[str, Sequence[str]]:
if self.outputs_by_type_cache is None:
self.outputs_by_type_cache = sort_regs_by_type(self.outputs)
return self.outputs_by_type_cache
def conflicts_with(self, other) -> bool:
"""Are there RAW/WAR/WAW conflicts between the instructions?
NOTE: inputs and outputs don't include all the vector registers (e.g.
when LMUL > 1, vl8re8.v, vs8r.v), but this is not a problem here,
because only vector instructions access vector registers, and we don't
care if those are conflicting with each other, as they all go to the
same dispatch queue.
Args:
other: the other instruction to check conflicts with.
Returns:
True if the instructions have conflicts, otherwise False.
"""
return (overlaps(self.inputs, other.outputs) or
overlaps(self.outputs, other.inputs) or
overlaps(self.outputs, other.outputs))
def sort_regs_by_type(regs: Sequence[str]) -> Dict[str, Sequence[str]]:
res = {}
for reg in regs:
rf = register_type(reg)
res.setdefault(rf, collections.deque()).append(reg)
return res
def overlaps(xs: Sequence[str], ys: Sequence[str]) -> bool:
return any(x in ys for x in xs)
RE_VREG = re.compile(r"v\d+$")
RE_FREG = re.compile(r"f\d+$")
RE_XREG = re.compile(r"x\d+$")
def is_vector_register(r: str) -> bool:
"""Test whether a register is a vector register."""
# TODO(sflur): ideally this RISC-V specific code would be somewhere else
return bool(RE_VREG.match(r))
def is_float_register(r: str) -> bool:
"""Test whether a register is a floating point register."""
# TODO(sflur): ideally this RISC-V specific code would be somewhere else
return bool(RE_FREG.match(r))
def is_int_register(r: str) -> bool:
"""Test whether a register is a floating point register."""
# TODO(sflur): ideally this RISC-V specific code would be somewhere else
# note that this code assumes that ABI names like "ra" have been replaced
# with architectural names like "x1"
return bool(RE_XREG.match(r))
def register_type(r: str) -> str:
"""Assign a register to a register class.
Args:
r: register id
Returns:
- register file name
Classes are
- "V": vector RF
- "F": float RF
- "X": general purpose (integer) RF
- "MISC: everything else
The MISC class includes various status and control registers
and we might want to split it into finer divisions.
"""
# TODO(sflur): instead of using hardcoded predicates this should be somehow
# configurable. There's already something in the uarch yaml file (see
# register_files), but I suspect using that will make things much slower. A
# posible solution is to hardcode some generic predicates (e.g. a
# letter+number) and configure those in the yaml. Also, I don't think a
# single config file should be used for gentrace and tbm, as gentrace
# config is more fixed.
if is_int_register(r):
return "X"
if is_vector_register(r):
return "V"
if is_float_register(r):
return "F"
return "MISC"