This is an automated email from the ASF dual-hosted git repository. He-Pin pushed a commit to branch iterator-getshort-int-long-fastpath in repository https://gitbox.apache.org/repos/asf/pekko.git
commit 97f7788fb9c5d1d8333d0648c9f6dfbdee68caa9 Author: He-Pin <[email protected]> AuthorDate: Sat May 9 21:55:53 2026 +0800 perf: fast path getShort/Int/Long in MultiByteArrayIterator 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]
