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 {


Reply via email to