From ab1674273416cfaff1182fd216ac6ae52bd41e45 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Sat, 22 Aug 2026 00:54:01 +0100 Subject: [PATCH 1/2] use SWAR in CBORParser --- .../jackson/dataformat/cbor/CBORParser.java | 95 +++++++++++++------ .../dataformat/cbor/CBORVarHandleUtil.java | 24 ++++- 2 files changed, 89 insertions(+), 30 deletions(-) diff --git a/cbor/src/main/java/tools/jackson/dataformat/cbor/CBORParser.java b/cbor/src/main/java/tools/jackson/dataformat/cbor/CBORParser.java index 36b044653..cf8f83ecc 100644 --- a/cbor/src/main/java/tools/jackson/dataformat/cbor/CBORParser.java +++ b/cbor/src/main/java/tools/jackson/dataformat/cbor/CBORParser.java @@ -125,6 +125,29 @@ public int getFirstTag() { protected final static JacksonFeatureSet CBOR_READ_CAPABILITIES = DEFAULT_READ_CAPABILITIES.with(StreamReadCapability.EXACT_FLOATS); + /** + * Whether VarHandles are usable on this runtime; probed once at class load. + * Being {@code static final} lets the branches in {@link #_decode32Bits()}, + * {@link #_decode64Bits()} and {@link #_decodeQuad} 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 `CBORVarHandleUtil`, 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 CBORVarHandleUtil.isAvailable(); + } catch (Throwable t) { + return false; + } + } + /* /********************************************************************** /* Configuration @@ -1566,10 +1589,8 @@ private final int _nextFieldOptimized(PropertyNameMatcher matcher, final int len int inPtr = _inputPtr; // First quadbyte is easy - int q1 = (inBuf[inPtr++] & 0xFF); - q1 = (q1 << 8) | (inBuf[inPtr++] & 0xFF); - q1 = (q1 << 8) | (inBuf[inPtr++] & 0xFF); - q1 = (q1 << 8) | (inBuf[inPtr++] & 0xFF); + int q1 = _decodeQuad(inBuf, inPtr); + inPtr += 4; if (len < 9) { int q2 = (inBuf[inPtr++] & 0xFF); @@ -1588,10 +1609,8 @@ private final int _nextFieldOptimized(PropertyNameMatcher matcher, final int len return matcher.matchByQuad(q1, q2); } - int q2 = (inBuf[inPtr++] & 0xFF); - q2 = (q2 << 8) | (inBuf[inPtr++] & 0xFF); - q2 = (q2 << 8) | (inBuf[inPtr++] & 0xFF); - q2 = (q2 << 8) | (inBuf[inPtr++] & 0xFF); + int q2 = _decodeQuad(inBuf, inPtr); + inPtr += 4; if (len < 13) { int q3 = (inBuf[inPtr++] & 0xFF); @@ -1636,11 +1655,8 @@ private final int _nextFieldFromSymbolsLong(PropertyNameMatcher matcher, final byte[] inBuf = _inputBuffer; do { - int q = (inBuf[inPtr++] & 0xFF); - q = (q << 8) | inBuf[inPtr++] & 0xFF; - q = (q << 8) | inBuf[inPtr++] & 0xFF; - q = (q << 8) | inBuf[inPtr++] & 0xFF; - _quadBuffer[offset++] = q; + _quadBuffer[offset++] = _decodeQuad(inBuf, inPtr); + inPtr += 4; } while ((len -= 4) > 3); // and then leftovers if (len > 0) { @@ -3356,10 +3372,8 @@ private final String _findDecodedFromSymbols(final int len) throws JacksonExcept int inPtr = _inputPtr; // First quadbyte is easy - int q1 = (inBuf[inPtr++] & 0xFF); - q1 = (q1 << 8) | (inBuf[inPtr++] & 0xFF); - q1 = (q1 << 8) | (inBuf[inPtr++] & 0xFF); - q1 = (q1 << 8) | (inBuf[inPtr++] & 0xFF); + int q1 = _decodeQuad(inBuf, inPtr); + inPtr += 4; if (len < 9) { int q2 = _padQuadForNulls(inBuf[inPtr++]); @@ -3378,10 +3392,8 @@ private final String _findDecodedFromSymbols(final int len) throws JacksonExcept return _symbols.findName(q1, q2); } - int q2 = (inBuf[inPtr++] & 0xFF); - q2 = (q2 << 8) | (inBuf[inPtr++] & 0xFF); - q2 = (q2 << 8) | (inBuf[inPtr++] & 0xFF); - q2 = (q2 << 8) | (inBuf[inPtr++] & 0xFF); + int q2 = _decodeQuad(inBuf, inPtr); + inPtr += 4; if (len < 13) { int q3 = _padQuadForNulls(inBuf[inPtr++]); @@ -3428,11 +3440,8 @@ private final String _findDecodedLong(int len, int q1, int q2) throws JacksonExc final byte[] inBuf = _inputBuffer; do { - int q = (inBuf[inPtr++] & 0xFF); - q = (q << 8) | inBuf[inPtr++] & 0xFF; - q = (q << 8) | inBuf[inPtr++] & 0xFF; - q = (q << 8) | inBuf[inPtr++] & 0xFF; - _quadBuffer[offset++] = q; + _quadBuffer[offset++] = _decodeQuad(inBuf, inPtr); + inPtr += 4; } while ((len -= 4) > 3); // and then leftovers if (len > 0) { @@ -3478,6 +3487,23 @@ private final static int _padQuadForNulls(int firstByte) { return (firstByte & 0xFF) | 0xFFFFFF00; } + /** + * Helper method for decoding 4 bytes of an Object property name into the + * "quad" (big-endian {@code int}) form used by {@code ByteQuadsCanonicalizer}. + * Caller MUST have verified that 4 bytes are readable at given offset. + * + * @since 3.3 + */ + private final static int _decodeQuad(byte[] buffer, int offset) { + if (_VARHANDLE_AVAILABLE) { + return CBORVarHandleUtil.getInt(buffer, offset); + } + return ((buffer[offset] & 0xFF) << 24) + | ((buffer[offset+1] & 0xFF) << 16) + | ((buffer[offset+2] & 0xFF) << 8) + | (buffer[offset+3] & 0xFF); + } + /* /********************************************************************** /* Internal methods, skipping @@ -3770,8 +3796,14 @@ private final int _decode32Bits() throws JacksonException { return _slow32(); } final byte[] b = _inputBuffer; - int v = (b[ptr++] << 24) + ((b[ptr++] & 0xFF) << 16) - + ((b[ptr++] & 0xFF) << 8) + (b[ptr++] & 0xFF); + final int v; + if (_VARHANDLE_AVAILABLE) { + v = CBORVarHandleUtil.getInt(b, ptr); + ptr += 4; + } else { + v = (b[ptr++] << 24) + ((b[ptr++] & 0xFF) << 16) + + ((b[ptr++] & 0xFF) << 8) + (b[ptr++] & 0xFF); + } _inputPtr = ptr; return v; } @@ -3801,6 +3833,13 @@ private final long _decode64Bits() throws JacksonException { return _slow64(); } final byte[] b = _inputBuffer; + if (_VARHANDLE_AVAILABLE) { + // NOTE: identical to `_long()` of the two 32-bit halves below, since + // that is just a big-endian 8-byte read spelled out + final long l = CBORVarHandleUtil.getLong(b, ptr); + _inputPtr = ptr + 8; + return l; + } int i1 = (b[ptr++] << 24) + ((b[ptr++] & 0xFF) << 16) + ((b[ptr++] & 0xFF) << 8) + (b[ptr++] & 0xFF); int i2 = (b[ptr++] << 24) + ((b[ptr++] & 0xFF) << 16) diff --git a/cbor/src/main/java/tools/jackson/dataformat/cbor/CBORVarHandleUtil.java b/cbor/src/main/java/tools/jackson/dataformat/cbor/CBORVarHandleUtil.java index 4cc74b553..ece81b047 100644 --- a/cbor/src/main/java/tools/jackson/dataformat/cbor/CBORVarHandleUtil.java +++ b/cbor/src/main/java/tools/jackson/dataformat/cbor/CBORVarHandleUtil.java @@ -51,8 +51,8 @@ static boolean isAvailable() { return LONG_BE != null; } - // Helper methods that write primitives via the class's own VarHandle fields. - // Only called when the corresponding field is non-null, which implies + // 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. static void setInt(byte[] array, int offset, int value) { @@ -62,4 +62,24 @@ static void setInt(byte[] array, int offset, int value) { static void setLong(byte[] array, int offset, long value) { LONG_BE.set(array, offset, value); } + + /** + * Reads 4 bytes at given offset as a big-endian {@code int}; caller MUST + * have verified that {@code offset+4} is within bounds of given array. + * + * @since 3.3 + */ + static int getInt(byte[] array, int offset) { + return (int) INT_BE.get(array, offset); + } + + /** + * Reads 8 bytes at given offset as a big-endian {@code long}; caller MUST + * have verified that {@code offset+8} is within bounds of given array. + * + * @since 3.3 + */ + static long getLong(byte[] array, int offset) { + return (long) LONG_BE.get(array, offset); + } } From 220990adcfe1b9f2b1963e2467ab7f0d7898288d Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 4 Sep 2026 16:01:56 -0700 Subject: [PATCH 2/2] Add release notes, update javadoc comments --- .../jackson/dataformat/cbor/CBORVarHandleUtil.java | 10 +++++----- release-notes/CREDITS | 3 +++ release-notes/VERSION | 2 ++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/cbor/src/main/java/tools/jackson/dataformat/cbor/CBORVarHandleUtil.java b/cbor/src/main/java/tools/jackson/dataformat/cbor/CBORVarHandleUtil.java index ece81b047..ffda34425 100644 --- a/cbor/src/main/java/tools/jackson/dataformat/cbor/CBORVarHandleUtil.java +++ b/cbor/src/main/java/tools/jackson/dataformat/cbor/CBORVarHandleUtil.java @@ -6,27 +6,27 @@ /** * Utility class that provides {@link VarHandle} instances for efficient - * multi-byte primitive writes to byte arrays. + * multi-byte primitive reads and writes on byte arrays. *

* 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 setXxx()} methods dereference the handles unconditionally, and the - * byte-shifting fallback lives in the caller, not here. + * {@code getXxx()} and {@code setXxx()} methods dereference the handles + * unconditionally, and the byte-shifting fallback lives in the caller, not here. * * @since 3.3 */ final class CBORVarHandleUtil { /** - * VarHandle for writing an {@code int} as 4 big-endian bytes. + * VarHandle for reading/writing an {@code int} as 4 big-endian bytes. * {@code null} if VarHandles are unavailable. */ static final VarHandle INT_BE; /** - * VarHandle for writing a {@code long} as 8 big-endian bytes. + * VarHandle for reading/writing a {@code long} as 8 big-endian bytes. * {@code null} if VarHandles are unavailable. */ static final VarHandle LONG_BE; diff --git a/release-notes/CREDITS b/release-notes/CREDITS index 71bf6021d..7fe9f080b 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -67,3 +67,6 @@ PJ Fanning (@pjfanning) * Contributed #757: (smile) Use `VarHandle` for multi-byte primitive reads in `SmileParser` (3.3.0) +* Contributed #758: (cbor) Use `VarHandle` for multi-byte primitive reads in + `CBORParser` + (3.3.0) diff --git a/release-notes/VERSION b/release-notes/VERSION index 5b450fc4d..e441ad982 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -25,6 +25,8 @@ implementations) (contributed by @pjfanning) #757: (smile) Use `VarHandle` for multi-byte primitive reads in `SmileParser` (contributed by @pjfanning) +#758: (cbor) Use `VarHandle` for multi-byte primitive reads in `CBORParser` + (contributed by @pjfanning) #759: (smile) Use more efficient `String` construction wrt "Compact Strings" for "long" text values (fix by @cowtowncoder, w/ Claude code)