From ecb1af719514da025577d4c67ab194c17d39728b Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Mon, 6 Apr 2026 10:42:51 -0500 Subject: [PATCH] Add fused SELECT_BOOL_REG to endpoints VM Optimizes a couple hot instructions for S3 endpoints, but is a generic optimization. For S3, these are optimized: ``` _s3e_fips = ite(UseFIPS, "-fips", "") _s3e_ds = ite(UseDualStack, ".dualstack", "") ``` Without SELECT_BOOL_REG, each compiles to 5 opcodes: ``` LOAD_REGISTER UseFIPS JMP_IF_FALSE 5 LOAD_CONST "-fips" JUMP 2 LOAD_CONST "" ``` With SELECT_BOOL_REG, each becomes 1 opcode: ``` SELECT_BOOL_REG UseFIPS, "-fips", "" ``` The end result is that this speeds up S3 endpoint resolution consistently by 1-4% depending on the test case (smaller programs like vanilla endpoint resolution see bigger savings, larger programs see less savings). --- .../java/rulesengine/BytecodeCompiler.java | 13 +++++++++++ .../rulesengine/BytecodeDisassembler.java | 23 +++++++++++++++++-- .../java/rulesengine/BytecodeEvaluator.java | 10 ++++++++ .../java/rulesengine/BytecodeWalker.java | 13 ++++++++++- .../smithy/java/rulesengine/Opcodes.java | 10 ++++++++ .../rulesengine/BytecodeCompilerTest.java | 3 +-- 6 files changed, 67 insertions(+), 5 deletions(-) diff --git a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeCompiler.java b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeCompiler.java index 458e61143..ab00d2989 100644 --- a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeCompiler.java +++ b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeCompiler.java @@ -328,6 +328,19 @@ public Void visitLibraryFunction(FunctionDefinition fn, List args) { throw new RulesEvaluationError("ite requires exactly 3 arguments, got " + args.size()); } + // Optimize: ite(boolRegister, constA, constB) -> SELECT_BOOL_REG + if (args.get(0) instanceof Reference ref + && args.get(1) instanceof Literal litTrue + && args.get(2) instanceof Literal litFalse) { + var trueVal = litTrue.toNode().toNode(); + var falseVal = litFalse.toNode().toNode(); + writer.writeByte(Opcodes.SELECT_BOOL_REG); + writer.writeByte(registerAllocator.getRegister(ref.getName().toString())); + writer.writeShort(writer.getConstantIndex(EndpointUtils.convertNode(trueVal, true))); + writer.writeShort(writer.getConstantIndex(EndpointUtils.convertNode(falseVal, true))); + return null; + } + // Compile condition compileExpression(args.get(0)); diff --git a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeDisassembler.java b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeDisassembler.java index 47b47cb68..22b532293 100644 --- a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeDisassembler.java +++ b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeDisassembler.java @@ -84,7 +84,8 @@ final class BytecodeDisassembler { Map.entry(Opcodes.JMP_IF_FALSE, new InstructionDef("JMP_IF_FALSE", Show.JUMP_OFFSET)), Map.entry(Opcodes.JUMP, new InstructionDef("JUMP", Show.JUMP_OFFSET)), Map.entry(Opcodes.SUBSTRING_EQ, new InstructionDef("SUBSTRING_EQ", Show.SUBSTRING_EQ)), - Map.entry(Opcodes.SPLIT_GET, new InstructionDef("SPLIT_GET", Show.SPLIT_GET))); + Map.entry(Opcodes.SPLIT_GET, new InstructionDef("SPLIT_GET", Show.SPLIT_GET)), + Map.entry(Opcodes.SELECT_BOOL_REG, new InstructionDef("SELECT_BOOL_REG", Show.SELECT_BOOL))); private enum Show { CONST, @@ -99,7 +100,8 @@ private enum Show { PROPERTY, ARG_COUNT, SUBSTRING_EQ, - SPLIT_GET + SPLIT_GET, + SELECT_BOOL } private record InstructionDef(String name, Show show) { @@ -260,6 +262,7 @@ private void writeInstruction(StringBuilder s, BytecodeWalker walker, String ind || opcode == Opcodes.JNN_OR_POP || (opcode == Opcodes.GET_PROPERTY_REG && i == 1) + || (opcode == Opcodes.SELECT_BOOL_REG && i >= 1) || (opcode == Opcodes.RESOLVE_TEMPLATE && i == 1)) { s.append(String.format("%5d", value)); @@ -380,6 +383,22 @@ private void appendSymbolicInfo(StringBuilder s, BytecodeWalker walker, Show sho } s.append(")[").append(index).append("]"); } + case SELECT_BOOL -> { + int regIndex = walker.getOperand(0); + int trueIdx = walker.getOperand(1); + int falseIdx = walker.getOperand(2); + if (regIndex >= 0 && regIndex < bytecode.getRegisterDefinitions().length) { + s.append(bytecode.getRegisterDefinitions()[regIndex].name()); + } + s.append(" ? "); + if (trueIdx >= 0 && trueIdx < bytecode.getConstantPoolCount()) { + s.append(formatConstant(bytecode.getConstant(trueIdx))); + } + s.append(" : "); + if (falseIdx >= 0 && falseIdx < bytecode.getConstantPoolCount()) { + s.append(formatConstant(bytecode.getConstant(falseIdx))); + } + } } } diff --git a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeEvaluator.java b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeEvaluator.java index a0be0bf15..0d02033e6 100644 --- a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeEvaluator.java +++ b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeEvaluator.java @@ -459,6 +459,16 @@ private Object runLoop(byte[] instructions, RulesFunction[] functions, Object[] var sgDelimiter = (String) constantPool[sgDelimIdx]; push(EndpointUtils.splitGet(sgValue, sgDelimiter, sgIndex)); } + case Opcodes.SELECT_BOOL_REG -> { + var selVal = registers[instructions[pc++] & 0xFF]; + int selTrue = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); + pc += 2; + int selFalse = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); + pc += 2; + push(selVal != null && selVal != Boolean.FALSE + ? constantPool[selTrue] + : constantPool[selFalse]); + } default -> throw new RulesEvaluationError("Unknown rules engine instruction: " + opcode, pc); } } diff --git a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeWalker.java b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeWalker.java index e6ee03547..1ca47e015 100644 --- a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeWalker.java +++ b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeWalker.java @@ -96,7 +96,7 @@ public int getOperandCount() { case Opcodes.GET_PROPERTY_REG, Opcodes.GET_INDEX_REG, Opcodes.RESOLVE_TEMPLATE, Opcodes.GET_NEGATIVE_INDEX_REG -> 2; - case Opcodes.SUBSTRING, Opcodes.SPLIT_GET -> 3; + case Opcodes.SUBSTRING, Opcodes.SPLIT_GET, Opcodes.SELECT_BOOL_REG -> 3; case Opcodes.SUBSTRING_EQ -> 5; default -> -1; }; @@ -206,6 +206,16 @@ public int getOperand(int index) { return code.get(pc + 4) & 0xFF; // index (will be cast to signed byte by caller) } break; + + case Opcodes.SELECT_BOOL_REG: + if (index == 0) { + return code.get(pc + 1) & 0xFF; // register + } else if (index == 1) { + return ((code.get(pc + 2) & 0xFF) << 8) | (code.get(pc + 3) & 0xFF); // true const + } else if (index == 2) { + return ((code.get(pc + 4) & 0xFF) << 8) | (code.get(pc + 5) & 0xFF); // false const + } + break; } throw new IllegalArgumentException("Invalid operand index " + index + " for opcode " + opcode); @@ -242,6 +252,7 @@ public static int getInstructionLength(byte opcode) { 3; case Opcodes.RESOLVE_TEMPLATE, Opcodes.GET_PROPERTY_REG, Opcodes.SUBSTRING -> 4; case Opcodes.SPLIT_GET -> 5; + case Opcodes.SELECT_BOOL_REG -> 6; case Opcodes.SUBSTRING_EQ -> 7; default -> -1; }; diff --git a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/Opcodes.java b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/Opcodes.java index 2103e02f9..fbee561bd 100644 --- a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/Opcodes.java +++ b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/Opcodes.java @@ -487,4 +487,14 @@ private Opcodes() {} *

Index is signed: positive = from start, negative = from end (-1 = last) */ public static final byte SPLIT_GET = 49; + + /** + * Select one of two constant values based on a boolean register. + * Fuses ite(register, constA, constB) into a single opcode. + * + *

Stack: [...] => [..., value] + * + *

SELECT_BOOL_REG [register:byte] [true-const:short] [false-const:short] + */ + public static final byte SELECT_BOOL_REG = 50; } diff --git a/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/BytecodeCompilerTest.java b/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/BytecodeCompilerTest.java index 08193b564..a0aa79f8a 100644 --- a/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/BytecodeCompilerTest.java +++ b/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/BytecodeCompilerTest.java @@ -647,8 +647,7 @@ void testCompileIte() { Bytecode bytecode = compiler.compile(); - assertOpcodePresent(bytecode, Opcodes.JMP_IF_FALSE); - assertOpcodePresent(bytecode, Opcodes.JUMP); + assertOpcodePresent(bytecode, Opcodes.SELECT_BOOL_REG); } @Test