Files
Odin/core/rexcode/isa/arm64/instructions.odin
Brendan Punsky c67282dfbd rexcode/arm64: an instruction can hold five operands
SME's outer products take five -- `smopa za0.s, p0/m, p1/m, z0.b, z1.b`
-- and Instruction held four, so the 14 forms in that family could not
be represented at all, let alone printed. They named one vector where
the instruction multiplies two.

Five Operands is 55 bytes, which is odd, so Mnemonic's alignment costs
one more; three bytes of padding still land Instruction on exactly one
64-byte cache line, as before. Encoding and Decode_Entry each grow by
two.

The tile number was wrong as well: it sits in the low bits, as wide as
the element size leaves room for -- three for .d down to none for .b --
not at bits 23:22 where the encoding read it. Every MOP form named a
tile it was not writing.

Note for anyone regenerating: tablegen re-emits tables.odin from a
template inside gen.odin, so a struct change there has to go in the
template, and the package has to compile before tablegen can run at all.
Same for the builders.

SVE/SME2 against llvm-mc: 697 byte-exact and 0 mismatched, of 704.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UmHLRF11EoWwNWCJ7JGaA
2026-08-28 19:29:20 -04:00

129 lines
5.5 KiB
Odin

// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_arm64
// =============================================================================
// INSTRUCTION
// =============================================================================
Instruction_Flags :: bit_field u8 {
_: u8 | 8,
}
// Sized and aligned to a cache line, deliberately.
//
// The payload is 45 bytes -- shrinking Operand to 10 got it there -- but
// leaving the struct at 48 was measurably worse than padding it back out.
// With `#packed` the struct aligns to 1, so a 48-byte stride straddles a line
// boundary 75% of the time and a 64-byte one straddled 100% of the time (the
// heap base is not line-aligned either). Decode writes whole Instructions, and
// unaligned stores cost enough that on an i7-9750H this layout decodes ~21%
// faster than the 48-byte packed one -- while writing MORE bytes. The gap
// holds even when the whole array is L1-resident, so it is split-store cost at
// the store ports, not cache-line fetches.
//
// Encode is within 1% and a pure read traversal is ~9% slower at working sets
// past L2, both of which the decode win dwarfs for real workloads.
//
// The spare bytes are free: they cost nothing over a 48-byte struct that
// straddles, and new fields land in them without changing the layout.
Instruction :: struct #align(64) {
ops: [5]Operand `fmt:"v,operand_count"`, // 5 * size_of(Operand) = 55
mnemonic: Mnemonic, // 2
operand_count: u8, // 1
flags: Instruction_Flags, // 1
length: u8, // 1 -- always 4
// 55 is odd, so mnemonic's alignment costs a byte here; 3 more reach 64.
_: [3]u8,
}
#assert(size_of(Instruction) == 64)
#assert(align_of(Instruction) == 64)
// =============================================================================
// Builders -- the most common shapes; less-common forms can be built
// inline by the caller using the Instruction struct directly.
// =============================================================================
@(require_results)
inst_none :: #force_inline proc "contextless" (m: Mnemonic) -> Instruction {
return Instruction{mnemonic = m, operand_count = 0, length = 4}
}
// Single-register (e.g. BR, BLR).
@(require_results)
inst_r :: #force_inline proc "contextless" (m: Mnemonic, r: Register) -> Instruction {
return Instruction{mnemonic = m, operand_count = 1, length = 4,
ops = {op_reg(r), {}, {}, {}, {}}}
}
// 2-register (e.g. CLZ, RBIT).
@(require_results)
inst_r_r :: #force_inline proc "contextless" (m: Mnemonic, rd, rn: Register) -> Instruction {
return Instruction{mnemonic = m, operand_count = 2, length = 4,
ops = {op_reg(rd), op_reg(rn), {}, {}, {}}}
}
// 3-register (e.g. ADD shifted, MUL, UDIV, ASRV).
@(require_results)
inst_r_r_r :: #force_inline proc "contextless" (m: Mnemonic, rd, rn, rm: Register) -> Instruction {
return Instruction{mnemonic = m, operand_count = 3, length = 4,
ops = {op_reg(rd), op_reg(rn), op_reg(rm), {}, {}}}
}
// 4-register R4-type (MADD, MSUB, SMADDL, ...).
@(require_results)
inst_r_r_r_r :: #force_inline proc "contextless" (m: Mnemonic, rd, rn, rm, ra: Register) -> Instruction {
return Instruction{mnemonic = m, operand_count = 4, length = 4,
ops = {op_reg(rd), op_reg(rn), op_reg(rm), op_reg(ra), {}}}
}
// 2-register + immediate (e.g. ADD imm).
@(require_results)
inst_r_r_i :: #force_inline proc "contextless" (m: Mnemonic, rd, rn: Register, imm: i64) -> Instruction {
return Instruction{mnemonic = m, operand_count = 3, length = 4,
ops = {op_reg(rd), op_reg(rn), op_imm(imm), {}, {}}}
}
// 1-register + immediate (e.g. MOVZ).
@(require_results)
inst_r_i :: #force_inline proc "contextless" (m: Mnemonic, rd: Register, imm: i64) -> Instruction {
return Instruction{mnemonic = m, operand_count = 2, length = 4,
ops = {op_reg(rd), op_imm(imm), {}, {}, {}}}
}
// MOVZ/MOVN/MOVK with explicit hw shift (0/16/32/48).
@(require_results)
inst_mov_imm :: #force_inline proc "contextless" (m: Mnemonic, rd: Register, imm: i64, hw: u8) -> Instruction {
return Instruction{mnemonic = m, operand_count = 3, length = 4,
ops = {op_reg(rd), op_imm(imm), op_imm(i64(hw), 1), {}, {}}}
}
// Load/store register: Rt + memory.
@(require_results)
inst_ldst :: #force_inline proc "contextless" (m: Mnemonic, rt: Register, mm: Memory) -> Instruction {
return Instruction{mnemonic = m, operand_count = 2, length = 4,
ops = {op_reg(rt), op_mem(mm), {}, {}, {}}}
}
// Load/store pair: Rt, Rt2, memory.
@(require_results)
inst_ldp_stp :: #force_inline proc "contextless" (m: Mnemonic, rt, rt2: Register, mm: Memory) -> Instruction {
return Instruction{mnemonic = m, operand_count = 3, length = 4,
ops = {op_reg(rt), op_reg(rt2), op_mem(mm), {}, {}}}
}
// PC-relative branch (B, BL).
@(require_results)
inst_branch :: #force_inline proc "contextless" (m: Mnemonic, label_id: u32) -> Instruction {
return Instruction{mnemonic = m, operand_count = 1, length = 4,
ops = {op_label(label_id, 4), {}, {}, {}, {}}}
}
// NOTE: the conditional branches, inst_cbz (+cbnz), inst_tbz (+tbnz) and
// inst_csel (+csinc/csinv/csneg) are generated per-mnemonic in
// mnemonic_builders.odin, so the generator owns those names. A conditional
// branch is one builder per condition -- inst_b_le(label), inst_bc_ne(label)
// -- because the condition is part of the mnemonic, not an operand; the
// condition-operand builders are the select/compare family, which really do
// take one (inst_csinc(rd, rn, rm, cond)).