llvm-project/llvm/test/TableGen/VarLenDecoder.td
Rahul Joshi dafffe262d
[LLVM][MC][DecoderEmitter] Add support to specialize decoder per bitwidth (#154865)
This change adds an option to specialize decoders per bitwidth, which
can help reduce the (compiled) code size of the decoder code.

**Current state**:
Currently, the code generated by the decoder emitter consists of two key
functions: `decodeInstruction` which is the entry point into the
generated code and `decodeToMCInst` which is invoked when a decode op is
reached while traversing through the decoder table. Both functions are
templated on `InsnType` which is the raw instruction bits that are
supplied to `decodeInstruction`.

Several backends call `decodeInstruction` with different `InsnType`
types, leading to several template instantiations of these functions in
the final code. As an example, AMDGPU instantiates this function with
type `DecoderUInt128` type for decoding 96/128-bit instructions,
`uint64_t` for decoding 64-bit instructions, and `uint32_t` for decoding
32-bit instructions. Since there is just one `decodeToMCInst` in the
generated code, it has code that handles decoding for *all* instruction
sizes. However, the decoders emitted for different instructions sizes
rarely have any intersection with each other. That means, in the AMDGPU
case, the instantiation with InsnType == DecoderUInt128 has decoder code
for 32/64-bit instructions that is *never exercised*. Conversely, the
instantiation with InsnType == uint64_t has decoder code for
128/96/32-bit instructions that is never exercised. This leads to
unnecessary dead code in the generated disassembler binary (that the
compiler cannot eliminate by itself).

**New state**:
With this change, we introduce an option
`specialize-decoders-per-bitwidth`. Under this mode, the DecoderEmitter
will generate several versions of `decodeToMCInst` function, one for
each bitwidth. The code is still templated, but will require backends to
specify, for each `InsnType` used, the bitwidth of the instruction that
the type is used to represent using a type-trait `InsnBitWidth`. This
will enable the templated code to choose the right variant of
`decodeToMCInst`. Under this mode, a particular instantiation will only
end up instantiating a single variant of `decodeToMCInst` generated and
that will include only those decoders that are applicable to a single
bitwidth, resulting in elimination of the code duplication through
instantiation and a reduction in code size.

Additionally, under this mode, decoders are uniqued only within a given
bitwidth (as opposed to across all bitwidths without this option), so
the decoder index values assigned are smaller, and consume less bytes in
their ULEB128 encoding. As a result, the generated decoder tables can
also reduce in size.

Adopt this feature for the AMDGPU and RISCV backend. In a release build,
this results in a net 55% reduction in the .text size of
libLLVMAMDGPUDisassembler.so and a 5% reduction in the .rodata size. For
RISCV, which today uses a single `uint64_t` type, this results in a 3.7%
increase in code size (expected as we instantiate the code 3 times now).

Actual measured sizes are as follows:
```
Baseline commit: 72c04bb882ad70230bce309c3013d9cc2c99e9a7
Configuration: Ubuntu clang version 18.1.3, release build with asserts disabled.
 
AMDGPU        Before       After      Change
======================================================
.text         612327       275607     55% reduction
.rodata       369728       351336      5% reduction          

RISCV:
======================================================
.text          47407       49187      3.7% increase   
.rodata        35768       35839      0.1% increase
```
2025-09-01 13:44:18 -07:00

99 lines
3.7 KiB
TableGen

// RUN: llvm-tblgen -gen-disassembler -I %p/../../include %s | FileCheck %s --check-prefixes=CHECK,CHECK-SMALL
// RUN: llvm-tblgen -gen-disassembler --large-decoder-table -I %p/../../include %s | FileCheck %s --check-prefixes=CHECK,CHECK-LARGE
include "llvm/Target/Target.td"
def ArchInstrInfo : InstrInfo { }
def Arch : Target {
let InstructionSet = ArchInstrInfo;
}
def Reg : Register<"reg">;
def RegClass : RegisterClass<"foo", [i64], 0, (add Reg)>;
def GR64 : RegisterOperand<RegClass>;
class MyMemOperand<dag sub_ops> : Operand<iPTR> {
let MIOperandInfo = sub_ops;
dag Base;
dag Extension;
}
def MemOp16: MyMemOperand<(ops GR64:$reg, i16imm:$offset)>;
def MemOp32: MyMemOperand<(ops GR64:$reg, i32imm:$offset)>;
class MyVarInst<MyMemOperand memory_op> : Instruction {
dag Inst;
let OutOperandList = (outs GR64:$dst);
let InOperandList = (ins memory_op:$src);
}
def FOO16 : MyVarInst<MemOp16> {
let Inst = (ascend
(descend (operand "$dst", 3), 0b01000, (operand "$src.reg", 3)),
(slice "$src.offset", 15, 0, (decoder "myCustomDecoder"))
);
}
def FOO32 : MyVarInst<MemOp32> {
let Inst = (ascend
(descend (operand "$dst", 3), 0b01001,
(operand "$src.reg", 3, (decoder "myCustomDecoder"))),
(slice "$src.offset", 31, 16),
(slice "$src.offset", 15, 0)
);
}
// Instruction length table
// CHECK: InstrLenTable
// CHECK: 27,
// CHECK-NEXT: 43,
// CHECK-NEXT: };
// CHECK-SMALL: /* 0 */ MCD::OPC_ExtractField, 3, 5, // Inst{7-3} ...
// CHECK-SMALL-NEXT: /* 3 */ MCD::OPC_FilterValue, 8, 4, 0, // Skip to: 11
// CHECK-SMALL-NEXT: /* 7 */ MCD::OPC_Decode, {{[0-9]+}}, {{[0-9]+}}, 0, // Opcode: FOO16
// CHECK-SMALL-NEXT: /* 11 */ MCD::OPC_FilterValueOrFail, 9,
// CHECK-SMALL-NEXT: /* 13 */ MCD::OPC_Decode, {{[0-9]+}}, {{[0-9]+}}, 1, // Opcode: FOO32
// CHECK-SMALL-NEXT: };
// CHECK-LARGE: /* 0 */ MCD::OPC_ExtractField, 3, 5, // Inst{7-3} ...
// CHECK-LARGE-NEXT: /* 3 */ MCD::OPC_FilterValue, 8, 4, 0, 0, // Skip to: 12
// CHECK-LARGE-NEXT: /* 8 */ MCD::OPC_Decode, {{[0-9]+}}, {{[0-9]+}}, 0, // Opcode: FOO16
// CHECK-LARGE-NEXT: /* 12 */ MCD::OPC_FilterValueOrFail, 9,
// CHECK-LARGE-NEXT: /* 14 */ MCD::OPC_Decode, {{[0-9]+}}, {{[0-9]+}}, 1, // Opcode: FOO32
// CHECK-LARGE-NEXT: };
// CHECK: case 0:
// CHECK-NEXT: tmp = fieldFromInstruction(insn, 8, 3);
// CHECK-NEXT: if (!Check(S, DecodeRegClassRegisterClass(MI, tmp, Address, Decoder))) { return MCDisassembler::Fail; }
// CHECK-NEXT: tmp = fieldFromInstruction(insn, 0, 3);
// CHECK-NEXT: if (!Check(S, DecodeRegClassRegisterClass(MI, tmp, Address, Decoder))) { return MCDisassembler::Fail; }
// CHECK-NEXT: tmp = fieldFromInstruction(insn, 11, 16);
// CHECK-NEXT: if (!Check(S, myCustomDecoder(MI, tmp, Address, Decoder))) { return MCDisassembler::Fail; }
// CHECK-NEXT: return S;
// CHECK-NEXT: case 1:
// CHECK-NEXT: tmp = fieldFromInstruction(insn, 8, 3);
// CHECK-NEXT: if (!Check(S, DecodeRegClassRegisterClass(MI, tmp, Address, Decoder))) { return MCDisassembler::Fail; }
// CHECK-NEXT: tmp = fieldFromInstruction(insn, 0, 3);
// CHECK-NEXT: if (!Check(S, myCustomDecoder(MI, tmp, Address, Decoder))) { return MCDisassembler::Fail; }
// CHECK-NEXT: tmp = 0x0;
// CHECK-NEXT: insertBits(tmp, fieldFromInstruction(insn, 11, 16), 16, 16);
// CHECK-NEXT: insertBits(tmp, fieldFromInstruction(insn, 27, 16), 0, 16);
// CHECK-NEXT: MI.addOperand(MCOperand::createImm(tmp));
// CHECK-NEXT: return S;
// CHECK-LABEL: case MCD::OPC_ExtractField: {
// CHECK: makeUp(insn, Start + Len);
// CHECK-LABEL: case MCD::OPC_CheckField:
// CHECK-NEXT: case MCD::OPC_CheckFieldOrFail: {
// CHECK: makeUp(insn, Start + Len);
// CHECK-LABEL: case MCD::OPC_Decode: {
// CHECK: Len = InstrLenTable[Opc];
// CHECK-NEXT: makeUp(insn, Len);