This is an automated email from the ASF dual-hosted git repository.
sruehl pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git
The following commit(s) were added to refs/heads/develop by this push:
new 361e32201d perf(plc4j/spi/buffers): speed up byte-aligned integer
read/write (#2650)
361e32201d is described below
commit 361e32201df3c1ea00b0604b7f358d334d479a14
Author: LivingLikeKrillin <[email protected]>
AuthorDate: Tue Jul 21 18:51:14 2026 +0900
perf(plc4j/spi/buffers): speed up byte-aligned integer read/write (#2650)
* perf(plc4j/spi/buffers): speed up byte-aligned integer read/write
The byte buffers sit on every driver's hottest path (every primitive
field of every message). Measured with JMH on a 32x uint16 batch (JDK 21):
~6x faster reads, ~5.6x faster writes, ~38% less allocation, with all 775
buffer tests (incl. fuzz) and the Modbus parser/serializer suite green.
- AbstractBuffer: use ArrayDeque for the option-context stack instead of
java.util.Stack (a synchronized Vector). A buffer is a single-threaded,
per-message scratch object -- positionInBits and the backing array are
already unsynchronized -- so the monitor enter/exit on every getContext()
guarded nothing. (Changing the protected context field type is
binary-incompatible for subclasses that read it directly; all in-repo
buffer modules recompile.)
- ByteOrderBigEndian: expose a shared stateless INSTANCE and return it from
getByteOrder() instead of allocating one per field.
- Read/WriteBufferByteBased: add a zero-allocation fast path for whole-byte,
byte-aligned, big-endian, plain-binary integer fields (unsigned and signed
short/int/long) that reads/writes straight from the backing array. Gated
on the concrete EncodingUnsignedBinary / EncodingTwosComplement (not the
broader EncodingDefault) so BCD/float/etc. fall through to the slow path
unchanged; covered by new regression tests.
Signed-off-by: Jooyoung Jung <[email protected]>
* fix(plc4j/spi/buffers): correct byte alignment for non-byte-aligned
sub-buffers
isAligned() (used by the whole-byte arraycopy fast paths in
readBits/writeBits, and
now by the integer fast-path guards) tested only positionInBits, ignoring
startBit.
A sub-buffer created at a non-byte-aligned offset has a non-zero startBit
while its
own positionInBits is 0, so a whole-byte read/write took the byte-copy fast
path and
indexed the backing array by (startBit + positionInBits) / 8 -- truncating
the sub-byte
offset and reading/writing from the wrong byte. Test it on the absolute bit
index.
Also gate the integer fast-path guards on isAligned() (absolute) instead of
positionInBits alone, and use `getByteOrder() instanceof
ByteOrderBigEndian` instead
of reference-equality with the singleton, so an explicitly
big-endian-configured buffer
(ServiceLoader instance) is still eligible for the fast path.
Adds a regression test: a whole-byte unsigned read from a non-byte-aligned
sub-buffer
now returns the correct value.
Signed-off-by: Jooyoung Jung <[email protected]>
* perf(plc4j/spi/buffers): resolve encoding/byte order once per integer
field
The fast-path eligibility guards resolved the encoding and byte order, and
when the fast path was not taken (e.g. a little-endian buffer or a
non-binary
encoding) the slow path resolved both again -- an extra context/manager
lookup
per whole-byte aligned integer field. Resolve them once in each read/write
method and reuse the resolved values for both the eligibility check and the
slow path. The BE-specific guards collapse into a single structural
isByteAlignedWholeBytes check combined with instanceof tests on the already
resolved values. (EncodingRaw paths now resolve the byte order they do not
use -- a negligible constant -- in exchange for exactly-once resolution
everywhere else.)
Signed-off-by: Jooyoung Jung <[email protected]>
* fix(plc4j/spi/buffers): honour startBit in writeBit; gate the fast path
on exact codec classes
Two hardening items in the same alignment/fast-path family as the previous
commits:
- writeBit indexed the backing array by positionInBits alone, ignoring
startBit -- unlike readBit and the whole-byte arraycopy path in writeBits,
which both use the absolute bit index. A WriteBufferByteBased constructed
with a non-zero startBit therefore wrote from the start of the backing
array. Index by (startBit + positionInBits), mirroring readBit, with a
regression test (writeHonoursNonByteAlignedStartBit).
- The integer fast-path gates checked `instanceof EncodingUnsignedBinary` /
`EncodingTwosComplement` / `ByteOrderBigEndian`. The slow path dispatches
virtually, so a subclass registered through the Encoding/ByteOrder
managers
with overridden codec behaviour would have been silently bypassed whenever
a field was byte-aligned. Gate on the exact class (getClass() == ...)
instead: multiple instances remain eligible, subclasses fall through to
the
virtual-dispatch slow path.
Also document that writeAlignedBytesBE relies on the caller's
ensureAvailable(numBits) (it has no internal capacity check), and drop the
dead value mask in writeUnsignedShort's fast path (the range check already
guarantees a non-negative value, and the mask was inconsistent with the
other five write sites).
Signed-off-by: Jooyoung Jung <[email protected]>
---------
Signed-off-by: Jooyoung Jung <[email protected]>
---
.../plc4x/java/spi/buffers/api/AbstractBuffer.java | 11 ++-
.../buffers/bytebased/AbstractBufferByteBased.java | 36 ++++++++-
.../spi/buffers/bytebased/ReadBufferByteBased.java | 77 ++++++++++++++++--
.../buffers/bytebased/WriteBufferByteBased.java | 90 ++++++++++++++++++++--
.../bytebased/byteorder/ByteOrderBigEndian.java | 3 +
.../buffers/bytebased/ReadBufferByteBasedTest.java | 47 +++++++++++
.../bytebased/WriteBufferByteBasedTest.java | 42 ++++++++++
7 files changed, 287 insertions(+), 19 deletions(-)
diff --git
a/plc4j/spi/buffers/api/src/main/java/org/apache/plc4x/java/spi/buffers/api/AbstractBuffer.java
b/plc4j/spi/buffers/api/src/main/java/org/apache/plc4x/java/spi/buffers/api/AbstractBuffer.java
index 2abffcf7a5..54e8ce5e42 100644
---
a/plc4j/spi/buffers/api/src/main/java/org/apache/plc4x/java/spi/buffers/api/AbstractBuffer.java
+++
b/plc4j/spi/buffers/api/src/main/java/org/apache/plc4x/java/spi/buffers/api/AbstractBuffer.java
@@ -20,16 +20,21 @@ package org.apache.plc4x.java.spi.buffers.api;
import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException;
+import java.util.ArrayDeque;
+import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
-import java.util.Stack;
public abstract class AbstractBuffer implements Buffer {
- protected final Stack<WithOption[]> context;
+ // Used as a stack (push/pop/peek). ArrayDeque instead of java.util.Stack:
a buffer is a
+ // single-threaded, per-message scratch object (positionInBits and the
backing array are
+ // themselves unsynchronized), so Stack's synchronization (it extends the
synchronized Vector)
+ // guards nothing here while adding a monitor enter/exit to getContext()
on every field.
+ protected final Deque<WithOption[]> context;
public AbstractBuffer(WithOption... options) {
- context = new Stack<>();
+ context = new ArrayDeque<>();
context.push(options);
}
diff --git
a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/AbstractBufferByteBased.java
b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/AbstractBufferByteBased.java
index 111e5855b4..9017ec0c3f 100644
---
a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/AbstractBufferByteBased.java
+++
b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/AbstractBufferByteBased.java
@@ -65,7 +65,31 @@ public abstract class AbstractBufferByteBased extends
AbstractBuffer {
return byteOrder.get();
}
}
- return new ByteOrderBigEndian();
+ return ByteOrderBigEndian.INSTANCE;
+ }
+
+ // ---- Byte-aligned integer fast-path helpers (shared by Read/Write byte
buffers) ----
+
+ /**
+ * Structural fast-path eligibility for the byte-aligned integer fast
path: no per-field
+ * options, the cursor on an absolute byte boundary (see {@link
#isAligned()}), and a
+ * whole number of bytes requested. The caller combines this with
EXACT-CLASS checks
+ * ({@code getClass() == ...}, not {@code instanceof}) on the ALREADY
RESOLVED encoding/byte
+ * order (plain binary / two's-complement, big-endian) so resolution
happens exactly once per
+ * field, and a registered subclass with overridden codec behaviour falls
through to the
+ * virtual-dispatch slow path instead of being silently bypassed by the
fast path.
+ */
+ protected boolean isByteAlignedWholeBytes(int numBits, WithOption[]
options) {
+ // isAligned() tests the ABSOLUTE bit index (startBit +
positionInBits) — the same predicate the
+ // readBits/writeBits whole-byte fast paths use — so a
non-byte-aligned sub-buffer correctly
+ // falls through to the generic path (the aligned fast paths index by
(startBit+positionInBits)/8).
+ return options.length == 0 && isAligned() && (numBits & 7) == 0;
+ }
+
+ /** Big-endian two's-complement sign extension of the low {@code numBits}
of {@code raw}. */
+ protected static long signExtend(long raw, int numBits) {
+ int shift = 64 - numBits;
+ return (raw << shift) >> shift;
}
protected Optional<Encoding> getUnsignedIntegerEncoding(WithOption...
options) {
@@ -156,8 +180,16 @@ public abstract class AbstractBufferByteBased extends
AbstractBuffer {
}
}
+ /**
+ * Whether the current cursor sits on a byte boundary of the BACKING
ARRAY. This must be tested on
+ * the absolute bit index ({@code startBit + positionInBits}), not on
{@code positionInBits} alone:
+ * a sub-buffer created at a non-byte-aligned offset (see {@code
createSubBuffer}) has a non-zero
+ * {@code startBit} while its own {@code positionInBits} is 0. The
whole-byte {@code arraycopy}
+ * fast paths in {@code readBits}/{@code writeBits} index the backing
array by
+ * {@code (startBit + positionInBits) / 8}, so only absolute alignment
makes that copy correct.
+ */
protected boolean isAligned() {
- return (positionInBits % 8) == 0;
+ return ((startBit + positionInBits) % 8) == 0;
}
}
diff --git
a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferByteBased.java
b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferByteBased.java
index 380252533e..bf4089ddcd 100644
---
a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferByteBased.java
+++
b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferByteBased.java
@@ -21,11 +21,15 @@ package org.apache.plc4x.java.spi.buffers.bytebased;
import org.apache.plc4x.java.spi.buffers.api.ReadBuffer;
import org.apache.plc4x.java.spi.buffers.api.WithOption;
import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException;
+import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrder;
+import
org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderBigEndian;
import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderManager;
import org.apache.plc4x.java.spi.buffers.bytebased.encoding.Encoding;
import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingDefault;
import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingManager;
import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingRaw;
+import
org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingTwosComplement;
+import
org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingUnsignedBinary;
import java.math.BigDecimal;
import java.math.BigInteger;
@@ -91,6 +95,23 @@ public class ReadBufferByteBased extends
AbstractBufferByteBased implements Read
}
}
+ /**
+ * Reads {@code numBits} (a whole number of bytes) big-endian from the
current byte-aligned
+ * position into a long and advances the position. Callers gate this
behind the fast-path
+ * eligibility checks; no intermediate byte[] or per-field ByteOrder
object is allocated.
+ */
+ private long readAlignedBytesBE(int numBits) throws BufferException {
+ ensureAvailable(numBits);
+ int byteIndex = (startBit + positionInBits) / 8;
+ int numBytes = numBits / 8;
+ long v = 0;
+ for (int i = 0; i < numBytes; i++) {
+ v = (v << 8) | (buffer[byteIndex + i] & 0xFF);
+ }
+ positionInBits += numBits;
+ return v;
+ }
+
@Override
public byte readUnsignedByte(int numBits, WithOption... options) throws
BufferException {
if (numBits < 1 || numBits > 7) {
@@ -118,10 +139,17 @@ public class ReadBufferByteBased extends
AbstractBufferByteBased implements Read
Optional<Encoding> encodingOptional =
getUnsignedIntegerEncoding(options);
Encoding encoding = encodingOptional.orElseThrow(() -> new
BufferException("No encoding defined for unsigned integer values"));
+ ByteOrder byteOrder = getByteOrder(options);
+ if (isByteAlignedWholeBytes(numBits, options)
+ && encoding.getClass() == EncodingUnsignedBinary.class
+ && byteOrder.getClass() == ByteOrderBigEndian.class) {
+ return (short) readAlignedBytesBE(numBits);
+ }
+
if(encoding instanceof EncodingDefault encodingDefault) {
ensureAvailable(numBits);
byte[] bytes = readBits(numBits);
- bytes = getByteOrder(options).process(bytes);
+ bytes = byteOrder.process(bytes);
return encodingDefault.decodeShort(numBits, bytes);
} else if(encoding instanceof EncodingRaw encodingRaw) {
return encodingRaw.decodeShort(numBits, this);
@@ -137,10 +165,19 @@ public class ReadBufferByteBased extends
AbstractBufferByteBased implements Read
Optional<Encoding> encodingOptional =
getUnsignedIntegerEncoding(options);
Encoding encoding = encodingOptional.orElseThrow(() -> new
BufferException("No encoding defined for unsigned integer values"));
+ ByteOrder byteOrder = getByteOrder(options);
+ // Fast path: byte-aligned whole-byte plain-binary big-endian read
straight from the backing
+ // array; encoding/byte order are resolved once above and reused by
the slow path below.
+ if (isByteAlignedWholeBytes(numBits, options)
+ && encoding.getClass() == EncodingUnsignedBinary.class
+ && byteOrder.getClass() == ByteOrderBigEndian.class) {
+ return (int) readAlignedBytesBE(numBits);
+ }
+
if(encoding instanceof EncodingDefault encodingDefault) {
ensureAvailable(numBits);
byte[] bytes = readBits(numBits);
- bytes = getByteOrder(options).process(bytes);
+ bytes = byteOrder.process(bytes);
return encodingDefault.decodeInt(numBits, bytes);
} else if(encoding instanceof EncodingRaw encodingRaw) {
return encodingRaw.decodeInt(numBits, this);
@@ -156,10 +193,17 @@ public class ReadBufferByteBased extends
AbstractBufferByteBased implements Read
Optional<Encoding> encodingOptional =
getUnsignedIntegerEncoding(options);
Encoding encoding = encodingOptional.orElseThrow(() -> new
BufferException("No encoding defined for unsigned integer values"));
+ ByteOrder byteOrder = getByteOrder(options);
+ if (isByteAlignedWholeBytes(numBits, options)
+ && encoding.getClass() == EncodingUnsignedBinary.class
+ && byteOrder.getClass() == ByteOrderBigEndian.class) {
+ return readAlignedBytesBE(numBits);
+ }
+
if(encoding instanceof EncodingDefault encodingDefault) {
ensureAvailable(numBits);
byte[] bytes = readBits(numBits);
- bytes = getByteOrder(options).process(bytes);
+ bytes = byteOrder.process(bytes);
return encodingDefault.decodeLong(numBits, bytes);
} else if(encoding instanceof EncodingRaw encodingRaw) {
return encodingRaw.decodeLong(numBits, this);
@@ -213,10 +257,17 @@ public class ReadBufferByteBased extends
AbstractBufferByteBased implements Read
Optional<Encoding> encodingOptional =
getSignedIntegerEncoding(options);
Encoding encoding = encodingOptional.orElseThrow(() -> new
BufferException("No encoding defined for signed integer values"));
+ ByteOrder byteOrder = getByteOrder(options);
+ if (isByteAlignedWholeBytes(numBits, options)
+ && encoding.getClass() == EncodingTwosComplement.class
+ && byteOrder.getClass() == ByteOrderBigEndian.class) {
+ return (short) signExtend(readAlignedBytesBE(numBits), numBits);
+ }
+
if(encoding instanceof EncodingDefault encodingDefault) {
ensureAvailable(numBits);
byte[] bytes = readBits(numBits);
- bytes = getByteOrder(options).process(bytes);
+ bytes = byteOrder.process(bytes);
return encodingDefault.decodeShort(numBits, bytes);
} else if(encoding instanceof EncodingRaw encodingRaw) {
return encodingRaw.decodeShort(numBits, this);
@@ -232,10 +283,17 @@ public class ReadBufferByteBased extends
AbstractBufferByteBased implements Read
Optional<Encoding> encodingOptional =
getSignedIntegerEncoding(options);
Encoding encoding = encodingOptional.orElseThrow(() -> new
BufferException("No encoding defined for signed integer values"));
+ ByteOrder byteOrder = getByteOrder(options);
+ if (isByteAlignedWholeBytes(numBits, options)
+ && encoding.getClass() == EncodingTwosComplement.class
+ && byteOrder.getClass() == ByteOrderBigEndian.class) {
+ return (int) signExtend(readAlignedBytesBE(numBits), numBits);
+ }
+
if(encoding instanceof EncodingDefault encodingDefault) {
ensureAvailable(numBits);
byte[] bytes = readBits(numBits);
- bytes = getByteOrder(options).process(bytes);
+ bytes = byteOrder.process(bytes);
return encodingDefault.decodeInt(numBits, bytes);
} else if(encoding instanceof EncodingRaw encodingRaw) {
return encodingRaw.decodeInt(numBits, this);
@@ -251,10 +309,17 @@ public class ReadBufferByteBased extends
AbstractBufferByteBased implements Read
Optional<Encoding> encodingOptional =
getSignedIntegerEncoding(options);
Encoding encoding = encodingOptional.orElseThrow(() -> new
BufferException("No encoding defined for signed integer values"));
+ ByteOrder byteOrder = getByteOrder(options);
+ if (isByteAlignedWholeBytes(numBits, options)
+ && encoding.getClass() == EncodingTwosComplement.class
+ && byteOrder.getClass() == ByteOrderBigEndian.class) {
+ return signExtend(readAlignedBytesBE(numBits), numBits);
+ }
+
if(encoding instanceof EncodingDefault encodingDefault) {
ensureAvailable(numBits);
byte[] bytes = readBits(numBits);
- bytes = getByteOrder(options).process(bytes);
+ bytes = byteOrder.process(bytes);
return encodingDefault.decodeLong(numBits, bytes);
} else if(encoding instanceof EncodingRaw encodingRaw) {
return encodingRaw.decodeLong(numBits, this);
diff --git
a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/WriteBufferByteBased.java
b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/WriteBufferByteBased.java
index be82f1d3b4..035fb42d66 100644
---
a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/WriteBufferByteBased.java
+++
b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/WriteBufferByteBased.java
@@ -22,11 +22,14 @@ import org.apache.plc4x.java.spi.buffers.api.WithOption;
import org.apache.plc4x.java.spi.buffers.api.WriteBuffer;
import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException;
import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrder;
+import
org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderBigEndian;
import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderManager;
import org.apache.plc4x.java.spi.buffers.bytebased.encoding.Encoding;
import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingDefault;
import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingManager;
import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingRaw;
+import
org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingTwosComplement;
+import
org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingUnsignedBinary;
import java.math.BigDecimal;
import java.math.BigInteger;
@@ -48,8 +51,12 @@ public class WriteBufferByteBased extends
AbstractBufferByteBased implements Wri
@Override
public void writeBit(boolean value, WithOption... options) throws
BufferException {
ensureAvailable(1);
- int byteIndex = positionInBits / 8;
- int bitIndex = positionInBits % 8;
+ // Index by the ABSOLUTE bit position (startBit + positionInBits),
like readBit and the
+ // whole-byte arraycopy path in writeBits — a buffer constructed with
a non-zero startBit
+ // must not write from the start of the backing array.
+ int absoluteBitIndex = startBit + positionInBits;
+ int byteIndex = absoluteBitIndex / 8;
+ int bitIndex = absoluteBitIndex % 8;
if (value) {
buffer[byteIndex] |= (byte) (0x80 >> bitIndex);
}
@@ -82,6 +89,23 @@ public class WriteBufferByteBased extends
AbstractBufferByteBased implements Wri
}
}
+ /**
+ * Writes the low {@code numBits} (a whole number of bytes) of {@code
value} big-endian into the
+ * backing array at the current byte-aligned position and advances the
position. Callers gate this
+ * behind the fast-path eligibility checks; no intermediate byte[] or
ByteOrder object is allocated.
+ * Unlike {@code readAlignedBytesBE} this method performs NO capacity
check of its own — every
+ * caller invokes {@code ensureAvailable(numBits)} before the fast-path
branch, and that call must
+ * not be removed as "redundant" with the slow path's.
+ */
+ private void writeAlignedBytesBE(int numBits, long value) {
+ int byteIndex = (startBit + positionInBits) / 8;
+ int numBytes = numBits / 8;
+ for (int i = 0; i < numBytes; i++) {
+ buffer[byteIndex + i] = (byte) ((value >>> ((numBytes - 1 - i) *
8)) & 0xFF);
+ }
+ positionInBits += numBits;
+ }
+
@Override
public void writeUnsignedByte(int numBits, byte value, WithOption...
options) throws BufferException {
if (numBits < 1 || numBits > 7) {
@@ -123,10 +147,18 @@ public class WriteBufferByteBased extends
AbstractBufferByteBased implements Wri
Optional<Encoding> encodingOptional =
getUnsignedIntegerEncoding(options);
Encoding encoding = encodingOptional.orElseThrow(() -> new
BufferException("No encoding defined for unsigned integer values"));
+ ByteOrder byteOrder = getByteOrder(options);
+ if (isByteAlignedWholeBytes(numBits, options)
+ && encoding.getClass() == EncodingUnsignedBinary.class
+ && byteOrder.getClass() == ByteOrderBigEndian.class) {
+ writeAlignedBytesBE(numBits, value);
+ return;
+ }
+
if(encoding instanceof EncodingDefault encodingDefault) {
ensureAvailable(numBits);
byte[] bytes = encodingDefault.encodeShort(numBits, value);
- bytes = getByteOrder(options).process(bytes);
+ bytes = byteOrder.process(bytes);
writeBits(numBits, bytes);
} else if(encoding instanceof EncodingRaw encodingRaw) {
byte[] bytes = encodingRaw.encodeShort(numBits, value);
@@ -150,10 +182,20 @@ public class WriteBufferByteBased extends
AbstractBufferByteBased implements Wri
Optional<Encoding> encodingOptional =
getUnsignedIntegerEncoding(options);
Encoding encoding = encodingOptional.orElseThrow(() -> new
BufferException("No encoding defined for unsigned integer values"));
+ ByteOrder byteOrder = getByteOrder(options);
+ // Fast path: byte-aligned whole-byte plain-binary big-endian write
straight into the backing
+ // array; encoding/byte order are resolved once above and reused by
the slow path below.
+ if (isByteAlignedWholeBytes(numBits, options)
+ && encoding.getClass() == EncodingUnsignedBinary.class
+ && byteOrder.getClass() == ByteOrderBigEndian.class) {
+ writeAlignedBytesBE(numBits, value);
+ return;
+ }
+
if(encoding instanceof EncodingDefault encodingDefault) {
ensureAvailable(numBits);
byte[] bytes = encodingDefault.encodeInt(numBits, value);
- bytes = getByteOrder(options).process(bytes);
+ bytes = byteOrder.process(bytes);
writeBits(numBits, bytes);
} else if(encoding instanceof EncodingRaw encodingRaw) {
byte[] bytes = encodingRaw.encodeInt(numBits, value);
@@ -177,10 +219,18 @@ public class WriteBufferByteBased extends
AbstractBufferByteBased implements Wri
Optional<Encoding> encodingOptional =
getUnsignedIntegerEncoding(options);
Encoding encoding = encodingOptional.orElseThrow(() -> new
BufferException("No encoding defined for unsigned integer values"));
+ ByteOrder byteOrder = getByteOrder(options);
+ if (isByteAlignedWholeBytes(numBits, options)
+ && encoding.getClass() == EncodingUnsignedBinary.class
+ && byteOrder.getClass() == ByteOrderBigEndian.class) {
+ writeAlignedBytesBE(numBits, value);
+ return;
+ }
+
if(encoding instanceof EncodingDefault encodingDefault) {
ensureAvailable(numBits);
byte[] bytes = encodingDefault.encodeLong(numBits, value);
- bytes = getByteOrder(options).process(bytes);
+ bytes = byteOrder.process(bytes);
writeBits(numBits, bytes);
} else if(encoding instanceof EncodingRaw encodingRaw) {
byte[] bytes = encodingRaw.encodeLong(numBits, value);
@@ -259,10 +309,18 @@ public class WriteBufferByteBased extends
AbstractBufferByteBased implements Wri
Optional<Encoding> encodingOptional =
getSignedIntegerEncoding(options);
Encoding encoding = encodingOptional.orElseThrow(() -> new
BufferException("No encoding defined for signed integer values"));
+ ByteOrder byteOrder = getByteOrder(options);
+ if (isByteAlignedWholeBytes(numBits, options)
+ && encoding.getClass() == EncodingTwosComplement.class
+ && byteOrder.getClass() == ByteOrderBigEndian.class) {
+ writeAlignedBytesBE(numBits, value);
+ return;
+ }
+
if(encoding instanceof EncodingDefault encodingDefault) {
ensureAvailable(numBits);
byte[] bytes = encodingDefault.encodeShort(numBits, value);
- bytes = getByteOrder(options).process(bytes);
+ bytes = byteOrder.process(bytes);
writeBits(numBits, bytes);
} else if(encoding instanceof EncodingRaw encodingRaw) {
byte[] bytes = encodingRaw.encodeShort(numBits, value);
@@ -287,10 +345,18 @@ public class WriteBufferByteBased extends
AbstractBufferByteBased implements Wri
Optional<Encoding> encodingOptional =
getSignedIntegerEncoding(options);
Encoding encoding = encodingOptional.orElseThrow(() -> new
BufferException("No encoding defined for signed integer values"));
+ ByteOrder byteOrder = getByteOrder(options);
+ if (isByteAlignedWholeBytes(numBits, options)
+ && encoding.getClass() == EncodingTwosComplement.class
+ && byteOrder.getClass() == ByteOrderBigEndian.class) {
+ writeAlignedBytesBE(numBits, value);
+ return;
+ }
+
if(encoding instanceof EncodingDefault encodingDefault) {
ensureAvailable(numBits);
byte[] bytes = encodingDefault.encodeInt(numBits, value);
- bytes = getByteOrder(options).process(bytes);
+ bytes = byteOrder.process(bytes);
writeBits(numBits, bytes);
} else if(encoding instanceof EncodingRaw encodingRaw) {
byte[] bytes = encodingRaw.encodeInt(numBits, value);
@@ -315,10 +381,18 @@ public class WriteBufferByteBased extends
AbstractBufferByteBased implements Wri
Optional<Encoding> encodingOptional =
getSignedIntegerEncoding(options);
Encoding encoding = encodingOptional.orElseThrow(() -> new
BufferException("No encoding defined for signed integer values"));
+ ByteOrder byteOrder = getByteOrder(options);
+ if (isByteAlignedWholeBytes(numBits, options)
+ && encoding.getClass() == EncodingTwosComplement.class
+ && byteOrder.getClass() == ByteOrderBigEndian.class) {
+ writeAlignedBytesBE(numBits, value);
+ return;
+ }
+
if(encoding instanceof EncodingDefault encodingDefault) {
ensureAvailable(numBits);
byte[] bytes = encodingDefault.encodeLong(numBits, value);
- bytes = getByteOrder(options).process(bytes);
+ bytes = byteOrder.process(bytes);
writeBits(numBits, bytes);
} else if(encoding instanceof EncodingRaw encodingRaw) {
byte[] bytes = encodingRaw.encodeLong(numBits, value);
diff --git
a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/byteorder/ByteOrderBigEndian.java
b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/byteorder/ByteOrderBigEndian.java
index 462d367f58..6b97a98415 100644
---
a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/byteorder/ByteOrderBigEndian.java
+++
b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/byteorder/ByteOrderBigEndian.java
@@ -27,6 +27,9 @@ public class ByteOrderBigEndian implements ByteOrder {
private static final WithOption OPTION =
WithByteBasedOption.WithByteOrder(NAME);
+ /** Stateless (process() is the identity), so one shared instance is safe
to reuse. */
+ public static final ByteOrderBigEndian INSTANCE = new ByteOrderBigEndian();
+
public static WithOption optionByteOrderBigEndian() {
return OPTION;
}
diff --git
a/plc4j/spi/buffers/byte/src/test/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferByteBasedTest.java
b/plc4j/spi/buffers/byte/src/test/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferByteBasedTest.java
index f92a47d7ab..07183c195d 100644
---
a/plc4j/spi/buffers/byte/src/test/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferByteBasedTest.java
+++
b/plc4j/spi/buffers/byte/src/test/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferByteBasedTest.java
@@ -21,6 +21,7 @@ package org.apache.plc4x.java.spi.buffers.bytebased;
import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException;
import
org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderBigEndian;
import
org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderLittleEndian;
+import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingBCD;
import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingIEEE754;
import
org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingTwosComplement;
import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingUTF8;
@@ -56,6 +57,52 @@ class ReadBufferByteBasedTest {
assertEquals(0, buffer.getRemainingBits());
}
+ // Guards the byte-aligned unsigned-int fast path: a BCD default encoding
must NOT be treated as
+ // plain binary. 0x12 0x34 decodes to 1234 (BCD), not 0x1234 = 4660
(binary shift).
+ @Test
+ void byteAlignedReadHonoursBcdEncodingNotBinaryFastPath() throws Exception
{
+ byte[] data = {0x12, 0x34};
+ ReadBufferByteBased buffer = new ReadBufferByteBased(data,
EncodingBCD.optionEncodingBCD());
+ assertEquals(1234, buffer.readUnsignedInt(16));
+ }
+
+ // The two's-complement byte-aligned fast path must sign-extend: 0xFFFE ->
-2, not 65534.
+ @Test
+ void byteAlignedSignedReadIsSignExtended() throws Exception {
+ ReadBufferByteBased b1 = new ReadBufferByteBased(new byte[]{(byte)
0xFF, (byte) 0xFE},
+ EncodingTwosComplement.optionEncodingTwosComplement());
+ assertEquals((short) -2, b1.readSignedShort(16));
+ ReadBufferByteBased b2 = new ReadBufferByteBased(new byte[]{(byte)
0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFE},
+ EncodingTwosComplement.optionEncodingTwosComplement());
+ assertEquals(-2, b2.readSignedInt(32));
+ }
+
+ // Positive counterpart to the BCD guard: the plain unsigned-binary
byte-aligned fast path must
+ // return the same value the slow path would. 0x12 0x34 -> 0x1234, 0x12
0x34 0x56 0x78 -> 0x12345678.
+ @Test
+ void byteAlignedUnsignedReadUsesFastPath() throws Exception {
+ ReadBufferByteBased b16 = new ReadBufferByteBased(new byte[]{0x12,
0x34},
+ EncodingUnsignedBinary.optionEncodingUnsignedBinary());
+ assertEquals(0x1234, b16.readUnsignedInt(16));
+ ReadBufferByteBased b32 = new ReadBufferByteBased(new byte[]{0x12,
0x34, 0x56, 0x78},
+ EncodingUnsignedBinary.optionEncodingUnsignedBinary());
+ assertEquals(0x12345678L, b32.readUnsignedLong(32));
+ }
+
+ // The fast path must key off the ABSOLUTE bit index (startBit +
positionInBits), not
+ // positionInBits alone. A sub-buffer created at a non-byte-aligned offset
has startBit % 8 != 0
+ // while its own positionInBits is 0; reading a whole-byte int must still
land on the right bits.
+ // bits 4..19 of A1 23 45 = 0001 0010 0011 0100 = 0x1234 (NOT bytes 0..1 =
0xA123).
+ @Test
+ void byteAlignedFastPathRespectsNonByteAlignedSubBufferStartBit() throws
Exception {
+ ReadBufferByteBased buffer = new ReadBufferByteBased(
+ new byte[]{(byte) 0xA1, 0x23, 0x45, 0x60},
+ EncodingUnsignedBinary.optionEncodingUnsignedBinary());
+ buffer.readUnsignedInt(4); // advance to
absolute bit 4
+ ReadBufferByteBased sub = buffer.createSubBuffer(16); // startBit =
4 (non-byte-aligned)
+ assertEquals(0x1234, sub.readUnsignedInt(16));
+ }
+
// readBits
@Test
void testReadBitsNestedSubBufferUnaligned() throws Exception {
diff --git
a/plc4j/spi/buffers/byte/src/test/java/org/apache/plc4x/java/spi/buffers/bytebased/WriteBufferByteBasedTest.java
b/plc4j/spi/buffers/byte/src/test/java/org/apache/plc4x/java/spi/buffers/bytebased/WriteBufferByteBasedTest.java
index da4743d7eb..d95ad1ebb4 100644
---
a/plc4j/spi/buffers/byte/src/test/java/org/apache/plc4x/java/spi/buffers/bytebased/WriteBufferByteBasedTest.java
+++
b/plc4j/spi/buffers/byte/src/test/java/org/apache/plc4x/java/spi/buffers/bytebased/WriteBufferByteBasedTest.java
@@ -22,6 +22,7 @@ import org.apache.plc4x.java.spi.buffers.api.WriteBuffer;
import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException;
import
org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderBigEndian;
import
org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderLittleEndian;
+import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingBCD;
import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingIEEE754;
import
org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingTwosComplement;
import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingUTF8;
@@ -56,6 +57,47 @@ class WriteBufferByteBasedTest {
assertEquals((byte) 0b10101100, result[0]);
}
+ // Guards the byte-aligned unsigned-int fast path: a BCD default encoding
must NOT be treated as
+ // plain binary. 1234 must be BCD-encoded to 0x12 0x34, not binary 0x04
0xD2.
+ @Test
+ void byteAlignedWriteHonoursBcdEncodingNotBinaryFastPath() throws
Exception {
+ WriteBufferByteBased buffer = new WriteBufferByteBased(new byte[2],
EncodingBCD.optionEncodingBCD());
+ buffer.writeUnsignedInt(16, 1234);
+ assertArrayEquals(new byte[]{0x12, 0x34}, buffer.getBytes());
+ }
+
+ // The two's-complement byte-aligned fast path must emit two's complement:
-2 -> 0xFF 0xFE.
+ @Test
+ void byteAlignedSignedWriteIsTwosComplement() throws Exception {
+ WriteBufferByteBased buffer = new WriteBufferByteBased(new byte[2],
EncodingTwosComplement.optionEncodingTwosComplement());
+ buffer.writeSignedShort(16, (short) -2);
+ assertArrayEquals(new byte[]{(byte) 0xFF, (byte) 0xFE},
buffer.getBytes());
+ }
+
+ // Positive counterpart to the BCD guard: the plain unsigned-binary
byte-aligned fast path must
+ // emit big-endian bytes. 0x1234 -> 0x12 0x34, 0x12345678 -> 0x12 0x34
0x56 0x78.
+ @Test
+ void byteAlignedUnsignedWriteUsesFastPath() throws Exception {
+ WriteBufferByteBased b16 = new WriteBufferByteBased(new byte[2],
EncodingUnsignedBinary.optionEncodingUnsignedBinary());
+ b16.writeUnsignedInt(16, 0x1234);
+ assertArrayEquals(new byte[]{0x12, 0x34}, b16.getBytes());
+ WriteBufferByteBased b32 = new WriteBufferByteBased(new byte[4],
EncodingUnsignedBinary.optionEncodingUnsignedBinary());
+ b32.writeUnsignedLong(32, 0x12345678L);
+ assertArrayEquals(new byte[]{0x12, 0x34, 0x56, 0x78}, b32.getBytes());
+ }
+
+ // Mirror of the read-side sub-buffer regression: writes must honour a
non-byte-aligned startBit.
+ // With startBit = 4 the position is not byte-aligned, so the write goes
bit-by-bit; writeBit must
+ // index the backing array by the ABSOLUTE bit position (startBit +
positionInBits), like readBit.
+ // 0x1234 into bits 4..19 of a zeroed 3-byte array = 0x01 0x23 0x40.
+ @Test
+ void writeHonoursNonByteAlignedStartBit() throws Exception {
+ WriteBufferByteBased buffer = new WriteBufferByteBased(new byte[3], 4,
16,
+ EncodingUnsignedBinary.optionEncodingUnsignedBinary());
+ buffer.writeUnsignedInt(16, 0x1234);
+ assertArrayEquals(new byte[]{0x01, 0x23, 0x40}, buffer.getBytes());
+ }
+
// writeBits
@Test
void testWriteBitsNestedSubBufferUnaligned() throws Exception {