From f7513b434bfb369d29a1432f704b0ae560f7de22 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Sat, 22 Aug 2026 09:30:43 +0100 Subject: [PATCH] use varhandles to to improve perf in Protobuf Parser and Generator --- .../protobuf/ProtobufGenerator.java | 46 ++++++++++ .../dataformat/protobuf/ProtobufParser.java | 38 ++++++++- .../protobuf/ProtobufVarHandleUtil.java | 84 +++++++++++++++++++ 3 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufVarHandleUtil.java diff --git a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufGenerator.java b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufGenerator.java index 951efdac3..7cbaa50fd 100644 --- a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufGenerator.java +++ b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufGenerator.java @@ -17,6 +17,28 @@ public class ProtobufGenerator extends GeneratorBase { + /** + * Whether VarHandles are usable on this runtime; probed once at class load. + * Being {@code static final} lets the branches in the {@code _writeIntXX()} + * helpers fold away at JIT time. + * + * @since 3.3 + */ + private static final boolean _VARHANDLE_AVAILABLE = _checkVarHandleAvailable(); + + private static boolean _checkVarHandleAvailable() { + // NOTE: this call is what first loads `ProtobufVarHandleUtil`, and that + // class names `VarHandle` in its field/method signatures. On a runtime + // without `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. + try { + return ProtobufVarHandleUtil.isAvailable(); + } catch (Throwable t) { + return false; + } + } + /* /********************************************************************** /* Constants @@ -1662,6 +1684,12 @@ private final void _writeInt32(int v) throws JacksonException _ensureRoom(9); // max tag 5 bytes int ptr = _writeTag(_currPtr); final byte[] buf = _currBuffer; + // protobuf fixed32 is little-endian + if (_VARHANDLE_AVAILABLE) { + ProtobufVarHandleUtil.setInt(buf, ptr, v); + _currPtr = ptr + 4; + return; + } buf[ptr++] = (byte) v; v >>= 8; buf[ptr++] = (byte) v; @@ -1677,6 +1705,12 @@ private final void _writeInt32NoTag(int v) throws JacksonException _ensureRoom(4); int ptr = _currPtr; final byte[] buf = _currBuffer; + // protobuf fixed32 is little-endian + if (_VARHANDLE_AVAILABLE) { + ProtobufVarHandleUtil.setInt(buf, ptr, v); + _currPtr = ptr + 4; + return; + } buf[ptr++] = (byte) v; v >>= 8; buf[ptr++] = (byte) v; @@ -1693,6 +1727,12 @@ private final void _writeInt64(long v64) throws JacksonException int ptr = _writeTag(_currPtr); final byte[] buf = _currBuffer; + // protobuf fixed64 is little-endian: low 32 bits first, then high + if (_VARHANDLE_AVAILABLE) { + ProtobufVarHandleUtil.setLong(buf, ptr, v64); + _currPtr = ptr + 8; + return; + } int v = (int) v64; buf[ptr++] = (byte) v; @@ -1722,6 +1762,12 @@ private final void _writeInt64NoTag(long v64) throws JacksonException int ptr = _currPtr; final byte[] buf = _currBuffer; + // protobuf fixed64 is little-endian: low 32 bits first, then high + if (_VARHANDLE_AVAILABLE) { + ProtobufVarHandleUtil.setLong(buf, ptr, v64); + _currPtr = ptr + 8; + return; + } int v = (int) v64; buf[ptr++] = (byte) v; diff --git a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java index 146823f60..1bc9ac4c7 100644 --- a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java +++ b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java @@ -16,6 +16,28 @@ public class ProtobufParser extends ParserMinimalBase { + /** + * Whether VarHandles are usable on this runtime; probed once at class load. + * Being {@code static final} lets the branches in {@link #_decode32Bits()} + * and {@link #_decode64Bits()} fold away at JIT time. + * + * @since 3.3 + */ + private static final boolean _VARHANDLE_AVAILABLE = _checkVarHandleAvailable(); + + private static boolean _checkVarHandleAvailable() { + // NOTE: this call is what first loads `ProtobufVarHandleUtil`, and that + // class names `VarHandle` in its field/method signatures. On a runtime + // without `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. + try { + return ProtobufVarHandleUtil.isAvailable(); + } catch (Throwable t) { + return false; + } + } + // State constants // State right after parser created; may start root Object @@ -2948,8 +2970,14 @@ protected final int _decode32Bits() throws JacksonException { return _slow32(); } final byte[] b = _inputBuffer; - int v = (b[ptr] & 0xFF) + ((b[ptr+1] & 0xFF) << 8) - + ((b[ptr+2] & 0xFF) << 16) + ((b[ptr+3] & 0xFF) << 24); + // protobuf fixed32 is little-endian + final int v; + if (_VARHANDLE_AVAILABLE) { + v = ProtobufVarHandleUtil.getInt(b, ptr); + } else { + v = (b[ptr] & 0xFF) + ((b[ptr+1] & 0xFF) << 8) + + ((b[ptr+2] & 0xFF) << 16) + ((b[ptr+3] & 0xFF) << 24); + } _inputPtr = ptr+4; return v; } @@ -2979,6 +3007,12 @@ protected final long _decode64Bits() throws JacksonException { return _slow64(); } final byte[] b = _inputBuffer; + // protobuf fixed64 is little-endian; `_long()` of the two 32-bit halves + // below is just that same little-endian 8-byte read spelled out + if (_VARHANDLE_AVAILABLE) { + _inputPtr = ptr+8; + return ProtobufVarHandleUtil.getLong(b, ptr); + } int i1 = (b[ptr++] & 0xFF) | ((b[ptr++] & 0xFF) << 8) | ((b[ptr++] & 0xFF) << 16) | (b[ptr++] << 24); int i2 = (b[ptr++] & 0xFF) | ((b[ptr++] & 0xFF) << 8) diff --git a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufVarHandleUtil.java b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufVarHandleUtil.java new file mode 100644 index 000000000..6376f5ab9 --- /dev/null +++ b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufVarHandleUtil.java @@ -0,0 +1,84 @@ +package tools.jackson.dataformat.protobuf; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.nio.ByteOrder; + +/** + * Utility class that provides {@link VarHandle} instances for efficient + * multi-byte primitive reads and writes on byte arrays. + *

+ * NOTE: handles here are LITTLE-endian, unlike the big-endian ones CBOR needs: + * protobuf encodes its {@code fixed32}/{@code fixed64} types (and hence + * {@code float}/{@code double}) as little-endian. + *

+ * Handles are resolved once at class initialization. On runtimes where + * {@code MethodHandles.byteArrayViewVarHandle()} is unsupported (for example + * some Android runtimes) they are left {@code null} and {@link #isAvailable()} + * returns {@code false}. Callers MUST check {@link #isAvailable()} first: the + * {@code getXxx()}/{@code setXxx()} methods dereference the handles + * unconditionally, and the byte-shifting fallback lives in the caller, not here. + *

+ * Callers must also invoke {@link #isAvailable()} from within a + * {@code try}/{@code catch (Throwable)} block: this class names {@link VarHandle} + * in its field and method signatures, so on a runtime lacking + * {@code java.lang.invoke.VarHandle} entirely it fails to link, raising + * {@link LinkageError} before any code here can run. + * + * @since 3.3 + */ +final class ProtobufVarHandleUtil +{ + /** + * VarHandle for reading/writing an {@code int} as 4 little-endian bytes. + * {@code null} if VarHandles are unavailable. + */ + static final VarHandle INT_LE; + + /** + * VarHandle for reading/writing a {@code long} as 8 little-endian bytes. + * {@code null} if VarHandles are unavailable. + */ + 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) { + // VarHandles not available (e.g., Android): fall back to manual byte shifting + } + INT_LE = intLe; + // assigned last: non-null implies every handle above resolved too + LONG_LE = longLe; + } + + private ProtobufVarHandleUtil() { } + + static boolean isAvailable() { + return LONG_LE != null; + } + + // Helper methods that read/write primitives via the class's own VarHandle + // fields. Only called when the corresponding field is non-null, which implies + // VarHandle is available on this runtime. Caller MUST also have verified that + // the full 4/8 bytes are within bounds of the given array. + + static int getInt(byte[] array, int offset) { + return (int) INT_LE.get(array, offset); + } + + static long getLong(byte[] array, int offset) { + return (long) LONG_LE.get(array, offset); + } + + static void setInt(byte[] array, int offset, int value) { + INT_LE.set(array, offset, value); + } + + static void setLong(byte[] array, int offset, long value) { + LONG_LE.set(array, offset, value); + } +}