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]