Minor changes in performance

This commit is contained in:
gingerBill
2026-06-15 20:36:11 +01:00
parent 7ef82c367c
commit 202be0601b
5 changed files with 98 additions and 103 deletions

View File

@@ -51,17 +51,8 @@ compile time — no table is built during a normal library build:
odin run <arch>/tablegen # ENCODING_TABLE -> generated Odin + <arch>/tables.odin
odin run <arch>/tablegen/generated # -> <arch>/tables/<arch>.*.bin
```
Regenerate after editing `ENCODING_TABLE`. See `docs/table_migration.md`.
## Performance (x86)
With `-o:speed -microarch:native -no-bounds-check`:
- Encoder: ~17 M instructions/sec (~56 MB/s)
- Decoder: ~16 M instructions/sec (~54 MB/s)
Measured on AMD Ryzen 3950X.
## Usage
```odin

View File

@@ -269,7 +269,7 @@ decode_opcode :: proc(state: ^Decoder_State) -> (entry: ^Decode_Entry, vex_entry
}
// Handle VEX/EVEX encoded instructions
if state.vex_type != .NONE {
if state.vex_type != nil {
return decode_opcode_vex(state)
}
@@ -575,8 +575,7 @@ decode_operands :: proc(state: ^Decoder_State, entry: ^Decode_Entry) -> (inst: I
// Check if we need ModR/M
needs_modrm := false
for _, i in entry.enc {
enc := entry.enc[i]
for enc in entry.enc {
if enc == .MR || enc == .REG || enc == .VVVV {
needs_modrm = true
break
@@ -617,25 +616,20 @@ decode_operands :: proc(state: ^Decoder_State, entry: ^Decode_Entry) -> (inst: I
}
// Decode each operand
for _, i in entry.ops {
op_type := entry.ops[i]
op_enc := entry.enc[i]
for op_type, i in entry.ops {
if op_type == .NONE {
break
}
op_enc := entry.enc[i]
// i386: default_64 entries have R64/RM64 operand types but
// really mean R32/RM32 in 32-bit mode (same encoded bytes).
effective := mode_rewrite_op_type(op_type, state.mode, entry.flags.default_64)
inst.ops[i], err = decode_single_operand(state, effective, op_enc, modrm_info, sib_info, has_sib)
if err != nil {
return {}, err
}
inst.ops[i] = decode_single_operand(state, effective, op_enc, modrm_info, sib_info, has_sib) or_return
inst.operand_count += 1
}
return inst, .NONE
return
}
decode_operands_vex :: proc(state: ^Decoder_State, entry: ^VEX_Decode_Entry) -> (inst: Instruction, err: Error_Code) {
@@ -664,30 +658,25 @@ decode_operands_vex :: proc(state: ^Decoder_State, entry: ^VEX_Decode_Entry) ->
}
// Decode each operand
for _, i in entry.ops {
op_type := entry.ops[i]
op_enc := entry.enc[i]
for op_type, i in entry.ops {
if op_type == .NONE {
break
}
op_enc := entry.enc[i]
inst.ops[i], err = decode_single_operand_vex(state, op_type, op_enc, modrm_info, sib_info, has_sib)
if err != nil {
return {}, err
}
inst.ops[i] = decode_single_operand_vex(state, op_type, op_enc, modrm_info, sib_info, has_sib) or_return
inst.operand_count += 1
}
return inst, .NONE
return
}
decode_single_operand :: proc(state: ^Decoder_State, op_type: Operand_Type, op_enc: Operand_Encoding,
modrm_info: ModRM_Info, sib_info: SIB_Info, has_sib: bool) -> (op: Operand, err: Error_Code) {
modrm_info: ModRM_Info, sib_info: SIB_Info, has_sib: bool) -> (op: Operand, err: Error_Code) {
switch op_enc {
case .NONE:
return {}, .NONE
return
case .REG:
// Register encoded in ModR/M.reg
@@ -696,7 +685,8 @@ decode_single_operand :: proc(state: ^Decoder_State, op_type: Operand_Type, op_e
register_number += 8
}
reg := decode_register(register_number, op_type, state.rex)
return op_reg(reg), .NONE
op = op_reg(reg)
return
case .MR:
// Register or memory in ModR/M.rm
@@ -707,7 +697,8 @@ decode_single_operand :: proc(state: ^Decoder_State, op_type: Operand_Type, op_e
register_number += 8
}
reg := decode_register(register_number, op_type, state.rex)
return op_reg(reg), .NONE
op = op_reg(reg)
return
} else {
// Memory
return decode_memory_operand(state, modrm_info, sib_info, has_sib, op_type)
@@ -716,43 +707,44 @@ decode_single_operand :: proc(state: ^Decoder_State, op_type: Operand_Type, op_e
case .IB:
// 8-bit immediate or rel8
if state.position >= len(state.data) {
return {}, .BUFFER_TOO_SHORT
err = .BUFFER_TOO_SHORT
return
}
immediate_value := i64(i8(state.data[state.position]))
state.position += 1
if op_type == .REL8 {
return Operand{kind = .RELATIVE, relative = immediate_value, size = 1}, .NONE
}
return Operand{kind = .IMMEDIATE, immediate = immediate_value, size = 1}, .NONE
op = Operand{kind = (op_type == .REL8 ? .RELATIVE : .IMMEDIATE), relative = immediate_value, size = 1}
return
case .IW:
// 16-bit immediate
if state.position + 2 > len(state.data) {
return {}, .BUFFER_TOO_SHORT
err = .BUFFER_TOO_SHORT
return
}
immediate_value := i64(i16(u16(state.data[state.position]) | u16(state.data[state.position+1]) << 8))
state.position += 2
return Operand{kind = .IMMEDIATE, immediate = immediate_value, size = 2}, .NONE
op = Operand{kind = .IMMEDIATE, immediate = immediate_value, size = 2}
return
case .ID:
// 32-bit immediate or rel32
if state.position + 4 > len(state.data) {
return {}, .BUFFER_TOO_SHORT
err = .BUFFER_TOO_SHORT
return
}
immediate_value := i64(i32(u32(state.data[state.position]) |
u32(state.data[state.position+1]) << 8 |
u32(state.data[state.position+2]) << 16 |
u32(state.data[state.position+3]) << 24))
state.position += 4
if op_type == .REL32 {
return Operand{kind = .RELATIVE, relative = immediate_value, size = 4}, .NONE
}
return Operand{kind = .IMMEDIATE, immediate = immediate_value, size = 4}, .NONE
op = Operand{kind = (op_type == .REL32 ? .RELATIVE : .IMMEDIATE), relative = immediate_value, size = 4}
return
case .IQ:
// 64-bit immediate
if state.position + 8 > len(state.data) {
return {}, .BUFFER_TOO_SHORT
err = .BUFFER_TOO_SHORT
return
}
immediate_value := i64(u64(state.data[state.position]) |
u64(state.data[state.position+1]) << 8 |
@@ -763,7 +755,8 @@ decode_single_operand :: proc(state: ^Decoder_State, op_type: Operand_Type, op_e
u64(state.data[state.position+6]) << 48 |
u64(state.data[state.position+7]) << 56)
state.position += 8
return Operand{kind = .IMMEDIATE, immediate = immediate_value, size = 8}, .NONE
op = Operand{kind = .IMMEDIATE, immediate = immediate_value, size = 8}
return
case .IMPL:
// Implicit register - decode from operand type
@@ -776,7 +769,8 @@ decode_single_operand :: proc(state: ^Decoder_State, op_type: Operand_Type, op_e
register_number += 8
}
reg := decode_register(register_number, op_type, state.rex)
return op_reg(reg), .NONE
op = op_reg(reg)
return
case .VVVV:
// VEX.vvvv register
@@ -785,25 +779,28 @@ decode_single_operand :: proc(state: ^Decoder_State, op_type: Operand_Type, op_e
register_number += 16
}
reg := decode_register(register_number, op_type, state.rex)
return op_reg(reg), .NONE
op = op_reg(reg)
return
case .IS4:
// Immediate byte with register in high 4 bits
if state.position >= len(state.data) {
return {}, .BUFFER_TOO_SHORT
err = .BUFFER_TOO_SHORT
return
}
immediate_byte := state.data[state.position]
state.position += 1
register_number := (immediate_byte >> 4) & 0x0F
reg := decode_register(register_number, op_type, state.rex)
return op_reg(reg), .NONE
op = op_reg(reg)
return
case .AAA:
// EVEX opmask - already decoded in state
return {}, .NONE
return
}
return {}, .NONE
return
}
decode_single_operand_vex :: proc(state: ^Decoder_State, op_type: Operand_Type, op_enc: Operand_Encoding,
@@ -940,7 +937,7 @@ decode_memory_operand :: proc(state: ^Decoder_State, modrm_info: ModRM_Info,
// 8.8 Register Decoding Helpers
// -----------------------------------------------------------------------------
decode_register :: proc(num: u8, op_type: Operand_Type, rex: u8) -> Register {
decode_register :: #force_inline proc "contextless" (num: u8, op_type: Operand_Type, rex: u8) -> Register {
#partial switch op_type {
case .R64, .RM64:
return gpr64_from_num(num)
@@ -1032,7 +1029,7 @@ decode :: proc(
}
data_length := len(data)
pos: u32 = 0
pos := 0
has_errors := false
// Track branch targets for label inference (resolved in pass 2 by isa).
@@ -1043,12 +1040,12 @@ decode :: proc(
// PASS 1: Decode all instructions, collect branch targets
// =========================================================================
for pos < u32(data_length) {
for pos < data_length {
inst: Instruction
info: Instruction_Info
// Record offset
info.offset = pos
info.offset = u32(pos)
// Initialize decoder state
state := Decoder_State{
@@ -1080,7 +1077,7 @@ decode :: proc(
is_dec := (b & 0x08) != 0
reg: Register = state.prefix_66 ? gpr16_from_num(reg_num) : gpr32_from_num(reg_num)
inst.mnemonic = is_dec ? Mnemonic.DEC : Mnemonic.INC
inst.mnemonic = is_dec ? .DEC : .INC
inst.operand_count = 1
inst.ops[0] = op_reg(reg)
inst.length = u8(state.position)
@@ -1095,7 +1092,7 @@ decode :: proc(
append(instructions, inst)
append(inst_info, info)
pos += u32(state.position)
pos += state.position
continue
}
}
@@ -1140,7 +1137,7 @@ decode :: proc(
info.rep = inst.flags.rep
info.segment = state.segment
info.vex_type = state.vex_type
if state.vex_type != .NONE && vex_entry != nil {
if state.vex_type != nil && vex_entry != nil {
// Use encoding requirements to distinguish LIG/WIG from L0/W0
// If encoding says LIG, the actual L value doesn't matter for re-encoding
// If encoding says L0/L1/L2, we should preserve the actual value
@@ -1157,7 +1154,7 @@ decode :: proc(
info.evex_b = state.evex_b
info.evex_z = state.evex_z
info.opmask = state.evex_aaa
} else if state.vex_type != .NONE {
} else if state.vex_type != nil {
// Fallback when vex_entry is nil (shouldn't happen normally)
info.vex_l = state.vex_l == 0 ? .L0 : (state.vex_l == 1 ? .L1 : .L2)
info.vex_w = state.vex_w ? .W1 : .W0
@@ -1167,7 +1164,7 @@ decode :: proc(
}
// Check for relative operands and record pending branch targets
inst_end := pos + u32(state.position)
inst_end := pos + state.position
for op_idx in 0..<inst.operand_count {
op := &inst.ops[op_idx]
if op.kind == .RELATIVE {
@@ -1186,17 +1183,17 @@ decode :: proc(
append(instructions, inst)
append(inst_info, info)
pos += u32(state.position)
pos += state.position
}
// =========================================================================
// PASS 2: Infer labels from branch targets within the decoded region
// =========================================================================
isa.infer_labels_from_branches(pending_branches[:], pos, label_defs, relocs)
isa.infer_labels_from_branches(pending_branches[:], u32(pos), label_defs, relocs)
return Result{
byte_count = pos,
byte_count = u32(pos),
success = !has_errors,
}
}

View File

@@ -705,7 +705,7 @@ encode :: proc(
// Check if instruction matches encoding (inlined for hot path).
// `mode` lets default_64 entries match 32-bit operands in i386 and
// filters out mode-restricted (mode_32_only) encodings when not in i386.
encoding_matches_inline :: #force_inline proc "contextless" (inst: ^Instruction, enc: ^Encoding, mode: Mode) -> bool {
encoding_matches_inline :: proc "contextless" (inst: ^Instruction, enc: ^Encoding, mode: Mode) -> bool {
// Mode gate: skip i386-only encodings (short-form INC/DEC at 0x40-0x4F)
// when not in Mode._32.
if enc.flags.mode_32_only && mode != ._32 { return false }
@@ -740,7 +740,7 @@ encoding_matches_inline :: #force_inline proc "contextless" (inst: ^Instruction,
if user_idx >= inst.operand_count - 1 { return false }
effective_op_type := mode_rewrite_op_type(op_type, mode, enc.flags.default_64)
if !operand_matches_inline(&inst.ops[user_idx], effective_op_type) { return false }
operand_matches_inline(&inst.ops[user_idx], effective_op_type) or_return
user_idx += 1
}
return user_idx == inst.operand_count - 1
@@ -757,7 +757,7 @@ encoding_matches_inline :: #force_inline proc "contextless" (inst: ^Instruction,
if user_idx >= inst.operand_count { return false }
effective_op_type := mode_rewrite_op_type(op_type, mode, enc.flags.default_64)
if !operand_matches_inline(&inst.ops[user_idx], effective_op_type) { return false }
operand_matches_inline(&inst.ops[user_idx], effective_op_type) or_return
user_idx += 1
}
@@ -861,16 +861,16 @@ imm_matches_inline :: #force_inline proc "contextless" (op: ^Operand, op_type: O
#partial switch op_type {
case .IMM8:
// Full 8-bit range: signed [-128, 127] OR unsigned [0, 255]
return op.immediate >= -128 && op.immediate <= 255
return -128 <= op.immediate && op.immediate <= 255
case .IMM8SX:
// Sign-extended 8-bit: must be in signed 8-bit range
return op.immediate >= -128 && op.immediate <= 127
return -128 <= op.immediate && op.immediate <= 127
case .IMM16:
// Full 16-bit range: signed [-32768, 32767] OR unsigned [0, 65535]
return op.immediate >= -32768 && op.immediate <= 65535
return -32768 <= op.immediate && op.immediate <= 65535
case .IMM32:
// Full 32-bit range: signed [-2147483648, 2147483647] OR unsigned [0, 4294967295]
return op.immediate >= -2147483648 && op.immediate <= 4294967295
return -2147483648 <= op.immediate && op.immediate <= 4294967295
case .IMM64:
return true // Any i64 value fits
}

View File

@@ -324,13 +324,13 @@ reg_needs_evex :: #force_inline proc "contextless" (r: Register) -> bool {
@(require_results)
reg_is_gpr :: #force_inline proc "contextless" (r: Register) -> bool {
c := reg_class(r)
return c >= REG_GPR64 && c <= REG_GPR8H
return REG_GPR64 <= c && c <= REG_GPR8H
}
@(require_results)
reg_is_vector :: #force_inline proc "contextless" (r: Register) -> bool {
c := reg_class(r)
return c >= REG_XMM && c <= REG_ZMM
return REG_XMM <= c && c <= REG_ZMM
}
@(require_results)
@@ -340,7 +340,7 @@ reg_is_high_byte :: #force_inline proc "contextless" (r: Register) -> bool {
// Size in bits for register
@(require_results)
reg_size :: proc "contextless" (r: Register) -> u16 {
reg_size :: #force_inline proc "contextless" (r: Register) -> u16 {
switch reg_class(r) {
case REG_GPR64: return 64
case REG_GPR32: return 32
@@ -382,19 +382,18 @@ gpr16_from_num :: #force_inline proc "contextless" (num: u8) -> Register {
return num < 16 ? Register(REG_GPR16 | u16(num)) : NONE
}
gpr8_from_num :: proc(num: u8, has_rex: bool) -> Register {
@(require_results)
gpr8_from_num :: #force_inline proc "contextless" (num: u8, has_rex: bool) -> Register {
// Without REX prefix, nums 4-7 encode AH/CH/DH/BH (high byte legacy regs)
// With REX prefix, nums 4-7 encode SPL/BPL/SIL/DIL (low byte regs)
if has_rex {
return num < 16 ? Register(REG_GPR8 | u16(num)) : NONE
} else {
if num < 4 {
return Register(REG_GPR8 | u16(num)) // AL, CL, DL, BL
} else if num < 8 {
return Register(REG_GPR8H | u16(num)) // AH, CH, DH, BH (hw num 4-7)
}
return NONE
} else if num < 4 {
return Register(REG_GPR8 | u16(num)) // AL, CL, DL, BL
} else if num < 8 {
return Register(REG_GPR8H | u16(num)) // AH, CH, DH, BH (hw num 4-7)
}
return NONE
}
@(require_results)

View File

@@ -6,7 +6,6 @@ import x86 "../"
import "../../isa"
import "core:fmt"
import "core:time"
import "core:slice"
import "core:strings"
import "core:mem/virtual"
import "core:math"
@@ -3051,7 +3050,7 @@ run_benchmarks :: proc() {
bench_insts := make([dynamic]x86.Instruction)
defer delete(bench_insts)
for _ in 0..<1000 {
for _ in 0..<100 {
insts := []x86.Instruction{
x86.inst_r(.PUSH, x86.RBP),
x86.inst_r_r(.MOV, x86.RBP, x86.RSP),
@@ -3071,25 +3070,34 @@ run_benchmarks :: proc() {
append(&bench_insts, ..insts)
}
code_buf: [16 * 1024]u8
code_buf := make([]byte, 1<<16)
defer delete(code_buf)
labels: [4]x86.Label_Definition
relocs: [dynamic]x86.Relocation; defer delete(relocs)
errs: [dynamic]x86.Error; defer delete(relocs)
insts: [dynamic]x86.Instruction; defer delete(insts)
info: [dynamic]x86.Instruction_Info; defer delete(info)
lbls: [dynamic]x86.Label_Definition; defer delete(lbls)
// Encode
enc_start := time.now()
enc_bytes := 0
for _ in 0..<ITERATIONS {
relocs: [dynamic]x86.Relocation; defer delete(relocs)
errs: [dynamic]x86.Error; defer delete(errs)
clear(&relocs)
clear(&errs)
result := x86.encode(bench_insts[:], labels[:], code_buf[:], &relocs, &errs, true, 0)
enc_bytes += int(result.byte_count)
}
enc_dur := time.duration_microseconds(time.since(enc_start))
enc_dur := time.duration_seconds(time.since(enc_start))
// Get encoded length for decode
encoded_len: u32
{
relocs: [dynamic]x86.Relocation; defer delete(relocs)
errs: [dynamic]x86.Error; defer delete(errs)
clear(&relocs)
clear(&errs)
result := x86.encode(bench_insts[:], labels[:], code_buf[:], &relocs, &errs, true, 0)
encoded_len = result.byte_count
}
@@ -3098,22 +3106,22 @@ run_benchmarks :: proc() {
dec_start := time.now()
dec_insts := 0
for _ in 0..<ITERATIONS {
insts: [dynamic]x86.Instruction; defer delete(insts)
info: [dynamic]x86.Instruction_Info; defer delete(info)
lbls: [dynamic]x86.Label_Definition; defer delete(lbls)
errs: [dynamic]x86.Error; defer delete(errs)
clear(&insts)
clear(&info)
clear(&lbls)
clear(&errs)
x86.decode(code_buf[:encoded_len], nil, &insts, &info, &lbls, &errs)
dec_insts += len(insts)
}
dec_dur := time.duration_microseconds(time.since(dec_start))
dec_dur := time.duration_seconds(time.since(dec_start))
enc_ips := f64(ITERATIONS * len(bench_insts)) / (enc_dur / 1_000_000)
dec_ips := f64(dec_insts) / (dec_dur / 1_000_000)
enc_mbps := f64(enc_bytes) / (enc_dur / 1_000_000) / 1_000_000
dec_mbps := f64(ITERATIONS * int(encoded_len)) / (dec_dur / 1_000_000) / 1_000_000
enc_ips := f64(ITERATIONS * len(bench_insts)) / enc_dur
dec_ips := f64(dec_insts) / dec_dur
enc_bps := u64(f64(enc_bytes) / enc_dur)
dec_bps := u64(f64(ITERATIONS * int(encoded_len)) / dec_dur)
fmt.printf(" Encoder: %.1f M insts/sec (%.1f MB/s)\n", enc_ips / 1_000_000, enc_mbps)
fmt.printf(" Decoder: %.1f M insts/sec (%.1f MB/s)\n", dec_ips / 1_000_000, dec_mbps)
fmt.printf(" Encoder: %.1f M insts/sec (%.1M/s)\n", enc_ips / 1_000_000, enc_bps)
fmt.printf(" Decoder: %.1f M insts/sec (%.1M/s)\n", dec_ips / 1_000_000, dec_bps)
}
// =============================================================================