This is an automated email from the ASF dual-hosted git repository.

pjfanning pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git


The following commit(s) were added to refs/heads/main by this push:
     new f481c59dd9 perf: fast path getShort/Int/Long in MultiByteArrayIterator 
(#2952)
f481c59dd9 is described below

commit f481c59dd9b992bfdd4d9ab90e443ece8f0d3c1f
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Sat May 9 22:56:16 2026 +0800

    perf: fast path getShort/Int/Long in MultiByteArrayIterator (#2952)
    
    Motivation:
    When a `MultiByteArrayIterator` is held and reused across many primitive
    reads on a multi-fragment ByteString, the inherited `getShort/Int/Long`
    implementations always fall through to the byte-by-byte path, missing the
    SWAR fast path that `ByteArrayIterator` already implements.
    
    Modification:
    Override `getShort`, `getInt`, and `getLong` on `MultiByteArrayIterator`.
    When the current fragment has enough bytes for the requested primitive,
    delegate to the current `ByteArrayIterator` (single SWAR read) and then
    normalize. Otherwise fall back to the existing byte-by-byte super impl,
    which already handles cross-fragment reads correctly. Mirrored to both
    the Scala 2.13 and Scala 3 sources.
    
    Result:
    Reused iterators on multi-fragment inputs avoid the byte-by-byte loop
    when the read does not cross a fragment boundary; cross-fragment reads
    keep the existing semantics.
---
 .../org/apache/pekko/util/ByteIteratorSpec.scala   | 43 ++++++++++++++++++++++
 .../org/apache/pekko/util/ByteIterator.scala       | 30 +++++++++++++++
 .../org/apache/pekko/util/ByteIterator.scala       | 30 +++++++++++++++
 3 files changed, 103 insertions(+)

diff --git 
a/actor-tests/src/test/scala/org/apache/pekko/util/ByteIteratorSpec.scala 
b/actor-tests/src/test/scala/org/apache/pekko/util/ByteIteratorSpec.scala
index 56fc8ca7e5..4539117a23 100644
--- a/actor-tests/src/test/scala/org/apache/pekko/util/ByteIteratorSpec.scala
+++ b/actor-tests/src/test/scala/org/apache/pekko/util/ByteIteratorSpec.scala
@@ -13,6 +13,8 @@
 
 package org.apache.pekko.util
 
+import java.nio.ByteOrder
+
 import org.apache.pekko.util.ByteIterator.ByteArrayIterator
 
 import org.scalatest.matchers.should.Matchers
@@ -43,5 +45,46 @@ class ByteIteratorSpec extends AnyWordSpec with Matchers {
       otherIndexOf(freshIterator(), 0x10, 1) should be(2)
       otherIndexOf(freshIterator(), 0x10, 3) should be(5)
     }
+
+    "match ByteArrayIterator semantics for getShort/Int/Long across both fast 
and cross-fragment paths" in {
+      // 16 bytes is large enough to fit a long (8B) entirely inside one 
fragment for the fast
+      // path AND to span every possible split for the cross-fragment path.
+      val bytes = Array.tabulate[Byte](16)(i => ((i * 31 + 7) & 0xFF).toByte)
+
+      def reference(off: Int, byteOrder: ByteOrder): (Short, Int, Long) = {
+        val it = ByteArrayIterator(bytes).drop(off)
+        val s = it.clone().getShort(byteOrder)
+        val i = it.clone().getInt(byteOrder)
+        val l = it.clone().getLong(byteOrder)
+        (s, i, l)
+      }
+
+      // Build a multi-fragment ByteString for every split point and read at 
every offset.
+      // Splits 1..15 produce two non-empty fragments; the .iterator on the 
result is a
+      // MultiByteArrayIterator. Reads where (off, off+primitiveSize) lies 
entirely inside
+      // a single fragment exercise the fast path; reads that straddle 
exercise super.
+      for (split <- 1 until bytes.length) {
+        val left = ByteString.fromArray(bytes, 0, split)
+        val right = ByteString.fromArray(bytes, split, bytes.length - split)
+        val combined = left ++ right
+
+        for (byteOrder <- Seq(ByteOrder.BIG_ENDIAN, ByteOrder.LITTLE_ENDIAN)) {
+          implicit val bo: ByteOrder = byteOrder
+          for (off <- 0 to bytes.length - java.lang.Long.BYTES) {
+            val (refS, refI, refL) = reference(off, byteOrder)
+
+            withClue(s"split=$split, off=$off, byteOrder=$byteOrder, getShort: 
") {
+              combined.iterator.drop(off).getShort shouldEqual refS
+            }
+            withClue(s"split=$split, off=$off, byteOrder=$byteOrder, getInt: 
") {
+              combined.iterator.drop(off).getInt shouldEqual refI
+            }
+            withClue(s"split=$split, off=$off, byteOrder=$byteOrder, getLong: 
") {
+              combined.iterator.drop(off).getLong shouldEqual refL
+            }
+          }
+        }
+      }
+    }
   }
 }
diff --git a/actor/src/main/scala-2.13/org/apache/pekko/util/ByteIterator.scala 
b/actor/src/main/scala-2.13/org/apache/pekko/util/ByteIterator.scala
index 75eb67ace4..64e09a563e 100644
--- a/actor/src/main/scala-2.13/org/apache/pekko/util/ByteIterator.scala
+++ b/actor/src/main/scala-2.13/org/apache/pekko/util/ByteIterator.scala
@@ -254,6 +254,36 @@ object ByteIterator {
       result
     }
 
+    // Fast path: when the current fragment has enough bytes for the requested 
primitive, delegate
+    // to the current ByteArrayIterator (which uses SWARUtil for a single 
read) instead of falling
+    // back to the byte-by-byte super impl. Cross-fragment reads keep the 
byte-by-byte path.
+    override def getShort(implicit byteOrder: ByteOrder): Short = {
+      val cur = current
+      if (cur.len >= java.lang.Short.BYTES) {
+        val r = cur.getShort(byteOrder)
+        normalize()
+        r
+      } else super.getShort(byteOrder)
+    }
+
+    override def getInt(implicit byteOrder: ByteOrder): Int = {
+      val cur = current
+      if (cur.len >= java.lang.Integer.BYTES) {
+        val r = cur.getInt(byteOrder)
+        normalize()
+        r
+      } else super.getInt(byteOrder)
+    }
+
+    override def getLong(implicit byteOrder: ByteOrder): Long = {
+      val cur = current
+      if (cur.len >= java.lang.Long.BYTES) {
+        val r = cur.getLong(byteOrder)
+        normalize()
+        r
+      } else super.getLong(byteOrder)
+    }
+
     final override def len: Int = iterators.foldLeft(0) { _ + _.len }
 
     final override def size: Int = {
diff --git a/actor/src/main/scala-3/org/apache/pekko/util/ByteIterator.scala 
b/actor/src/main/scala-3/org/apache/pekko/util/ByteIterator.scala
index 31518cd81d..794194f995 100644
--- a/actor/src/main/scala-3/org/apache/pekko/util/ByteIterator.scala
+++ b/actor/src/main/scala-3/org/apache/pekko/util/ByteIterator.scala
@@ -250,6 +250,36 @@ object ByteIterator {
       result
     }
 
+    // Fast path: when the current fragment has enough bytes for the requested 
primitive, delegate
+    // to the current ByteArrayIterator (which uses SWARUtil for a single 
read) instead of falling
+    // back to the byte-by-byte super impl. Cross-fragment reads keep the 
byte-by-byte path.
+    override def getShort(implicit byteOrder: ByteOrder): Short = {
+      val cur = current
+      if (cur.len >= java.lang.Short.BYTES) {
+        val r = cur.getShort(byteOrder)
+        normalize()
+        r
+      } else super.getShort(byteOrder)
+    }
+
+    override def getInt(implicit byteOrder: ByteOrder): Int = {
+      val cur = current
+      if (cur.len >= java.lang.Integer.BYTES) {
+        val r = cur.getInt(byteOrder)
+        normalize()
+        r
+      } else super.getInt(byteOrder)
+    }
+
+    override def getLong(implicit byteOrder: ByteOrder): Long = {
+      val cur = current
+      if (cur.len >= java.lang.Long.BYTES) {
+        val r = cur.getLong(byteOrder)
+        normalize()
+        r
+      } else super.getLong(byteOrder)
+    }
+
     final override def len: Int = iterators.foldLeft(0) { _ + _.len }
 
     final override def size: Int = {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to