From 17c2da497f5697a26bb9bc090b775f6f2e83058d Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Sat, 22 Aug 2026 09:20:22 +0100 Subject: [PATCH] use SWAR in Avro parser --- .../avro/deser/AvroVarHandleUtil.java | 86 +++++++++++++++++++ .../avro/deser/JacksonAvroParserImpl.java | 59 ++++++++++--- 2 files changed, 134 insertions(+), 11 deletions(-) create mode 100644 avro/src/main/java/tools/jackson/dataformat/avro/deser/AvroVarHandleUtil.java diff --git a/avro/src/main/java/tools/jackson/dataformat/avro/deser/AvroVarHandleUtil.java b/avro/src/main/java/tools/jackson/dataformat/avro/deser/AvroVarHandleUtil.java new file mode 100644 index 000000000..94b9f02bc --- /dev/null +++ b/avro/src/main/java/tools/jackson/dataformat/avro/deser/AvroVarHandleUtil.java @@ -0,0 +1,86 @@ +package tools.jackson.dataformat.avro.deser; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.nio.ByteOrder; + +/** + * Utility class that provides {@link VarHandle}-based access for reading + * multi-byte primitives out of byte arrays. + *

+ * NOTE: handles here are LITTLE-endian, unlike the big-endian ones other + * Jackson binary backends need: Avro encodes {@code float} and {@code double} + * as little-endian IEEE-754. + *

+ * IMPORTANT: this class references {@link VarHandle} in field and method + * signatures, so on a runtime that does not provide {@code java.lang.invoke.VarHandle} + * at all it will fail to link, before any code here gets a chance to run. + * Callers MUST therefore both: + *

    + *
  1. load this class from within a {@code try}/{@code catch (Throwable)} block, + * so that {@link LinkageError} is caught, and
  2. + *
  3. keep the byte-shifting fallback in a class that does not reference + * {@link VarHandle}, so the fallback path never resolves this class.
  4. + *
+ * {@code JacksonAvroParserImpl} does both; see it for the pattern. + * + * @since 3.3 + */ +final class AvroVarHandleUtil +{ + /** + * VarHandle for reading 4 little-endian bytes as an {@code int}. + * {@code null} if byte-array views are unsupported. + */ + private static final VarHandle INT_LE; + + /** + * VarHandle for reading 8 little-endian bytes as a {@code long}. + * {@code null} if byte-array views are unsupported. + */ + private static final VarHandle LONG_LE; + + static { + VarHandle intLe = null; + VarHandle longLe = null; + try { + intLe = MethodHandles.byteArrayViewVarHandle(int[].class, ByteOrder.LITTLE_ENDIAN); + longLe = MethodHandles.byteArrayViewVarHandle(long[].class, ByteOrder.LITTLE_ENDIAN); + } catch (Throwable t) { + // Byte-array views not supported: caller falls back to byte shifting + } + INT_LE = intLe; + // assigned last: non-null implies the handle above resolved too + LONG_LE = longLe; + } + + private AvroVarHandleUtil() { } + + /** + * @return {@code true} if the {@code getXxx()} methods may be called; if + * {@code false}, caller MUST use its own byte-shifting fallback + */ + static boolean isAvailable() { + return LONG_LE != null; + } + + // Helper methods that read primitives via the class's own VarHandle fields. + // Only called when {@link #isAvailable()} returned true; the handles are + // dereferenced unconditionally and the fallback lives in the caller. + + /** + * Reads 4 bytes at given offset as a little-endian {@code int}; caller MUST + * have verified that {@code offset+4} is within bounds of given array. + */ + static int getIntLE(byte[] array, int offset) { + return (int) INT_LE.get(array, offset); + } + + /** + * Reads 8 bytes at given offset as a little-endian {@code long}; caller MUST + * have verified that {@code offset+8} is within bounds of given array. + */ + static long getLongLE(byte[] array, int offset) { + return (long) LONG_LE.get(array, offset); + } +} diff --git a/avro/src/main/java/tools/jackson/dataformat/avro/deser/JacksonAvroParserImpl.java b/avro/src/main/java/tools/jackson/dataformat/avro/deser/JacksonAvroParserImpl.java index 287df3b02..5f52d5dac 100644 --- a/avro/src/main/java/tools/jackson/dataformat/avro/deser/JacksonAvroParserImpl.java +++ b/avro/src/main/java/tools/jackson/dataformat/avro/deser/JacksonAvroParserImpl.java @@ -40,7 +40,31 @@ public class JacksonAvroParserImpl extends AvroParserImpl } sUtf8UnitLengths = table; } - + + /** + * Whether VarHandles are usable on this runtime; probed once at class load. + * Being {@code static final} lets the branches in {@link #decodeFloat()} and + * {@link #decodeDouble()} fold away at JIT time. + * + * @since 3.3 + */ + private final static boolean _VARHANDLE_AVAILABLE = _checkVarHandleAvailable(); + + private static boolean _checkVarHandleAvailable() { + // NOTE: this call is what first loads `AvroVarHandleUtil`, and that class + // names `VarHandle` in its field/method signatures. On a runtime lacking + // `java.lang.invoke.VarHandle` (some Android builds) loading it raises + // `NoClassDefFoundError` -- an Error, not an Exception -- so `Throwable` + // is what has to be caught here. Without this guard the failure would + // propagate out of this class's initializer and make the parser unusable. + try { + return AvroVarHandleUtil.isAvailable(); + } catch (Throwable t) { + return false; + } + } + + /* /********************************************************************** /* Input source config @@ -523,8 +547,14 @@ public JsonToken decodeFloat() throws IOException { } final byte[] buf = _inputBuffer; _inputPtr = ptr+4; - int i = (buf[ptr] & 0xff) | ((buf[ptr+1] & 0xff) << 8) - | ((buf[ptr+2] & 0xff) << 16) | (buf[ptr+3] << 24); + // Avro encodes float as little-endian IEEE-754 + final int i; + if (_VARHANDLE_AVAILABLE) { + i = AvroVarHandleUtil.getIntLE(buf, ptr); + } else { + i = (buf[ptr] & 0xff) | ((buf[ptr+1] & 0xff) << 8) + | ((buf[ptr+2] & 0xff) << 16) | (buf[ptr+3] << 24); + } _numberFloat = Float.intBitsToFloat(i); _numTypesValid = NR_FLOAT; return JsonToken.VALUE_NUMBER_FLOAT; @@ -543,15 +573,22 @@ public JsonToken decodeDouble() throws IOException { ptr = _inputPtr; } final byte[] buf = _inputBuffer; - int i = (buf[ptr] & 0xff) | ((buf[ptr+1] & 0xff) << 8) - | ((buf[ptr+2] & 0xff) << 16) | (buf[ptr+3] << 24); - ptr += 4; - int i2 = (buf[ptr] & 0xff) | ((buf[ptr+1] & 0xff) << 8) - | ((buf[ptr+2] & 0xff) << 16) | (buf[ptr+3] << 24); - + // Avro encodes double as little-endian IEEE-754; the two 32-bit halves + // below combine to exactly a little-endian 8-byte read + final long l; + if (_VARHANDLE_AVAILABLE) { + l = AvroVarHandleUtil.getLongLE(buf, ptr); + ptr += 4; + } else { + int i = (buf[ptr] & 0xff) | ((buf[ptr+1] & 0xff) << 8) + | ((buf[ptr+2] & 0xff) << 16) | (buf[ptr+3] << 24); + ptr += 4; + int i2 = (buf[ptr] & 0xff) | ((buf[ptr+1] & 0xff) << 8) + | ((buf[ptr+2] & 0xff) << 16) | (buf[ptr+3] << 24); + l = (((long) i) & 0xffffffffL) | (((long) i2) << 32); + } _inputPtr = ptr+4; - _numberDouble = Double.longBitsToDouble((((long) i) & 0xffffffffL) - | (((long) i2) << 32)); + _numberDouble = Double.longBitsToDouble(l); _numTypesValid = NR_DOUBLE; return JsonToken.VALUE_NUMBER_FLOAT; }