perf: avoid redundant bounds checks on ByteString indexed reads - #3533
Open
pjfanning wants to merge 1 commit into
Open
perf: avoid redundant bounds checks on ByteString indexed reads#3533pjfanning wants to merge 1 commit into
pjfanning wants to merge 1 commit into
Conversation
Motivation: ByteString.apply(idx) bounds-checks before delegating to byteAtUnchecked, and for the ByteStrings composite it also resolves the absolute offset to a fragment. Two call sites under src/main pay that check when the caller has already established the bound: - ByteStringParser.ByteReader.readByte guards with `off < input.length` and then calls input(off), so the bound is tested twice. Its siblings readShortLE/readIntLE/readLongLE already use the *Unchecked variants after their own guard, so readByte was the odd one out. ByteReader backs gzip/deflate header parsing, TLS and framing. - TcpDnsClient.decodeLength read the two length-prefix bytes with two apply calls: two bounds checks, and for a composite ByteString two fragment lookups. These are the only two places under src/main that index a ByteString outside ByteString.scala itself and bench-jmh. Modification: readByte now calls input.byteAtUnchecked(off). `off` starts at 0 and only ever moves forward, so the existing guard is sufficient. decodeLength now uses data.readShortBE(0) & 0xFFFF. readShortBE keeps the bounds check, so short input still throws IndexOutOfBoundsException as before, and it is SWAR-optimised for the single-array ByteString1C/ByteString1 that the common single-chunk read path produces. Result: One bounds check per byte instead of two in the stream parser, and one checked 16-bit read instead of two indexed reads for the DNS length prefix. Behaviour is unchanged. Tests: - sbt "stream-tests/testOnly org.apache.pekko.stream.io.ByteStringParserSpec org.apache.pekko.stream.scaladsl.CompressionSpec org.apache.pekko.stream.io.compression.*" - 106 succeeded, 0 failed - sbt "actor-tests/testOnly org.apache.pekko.io.dns.internal.TcpDnsClientSpec" - 8 succeeded, 0 failed - sbt "actor/mimaReportBinaryIssues" "stream/mimaReportBinaryIssues" - success - Native scalafmt run on the two changed files. - No new tests: both changes are behaviour-preserving refactors of internal code, covered by the existing specs above. - Not benchmarked. The change is a strict reduction in work per read, not an algorithmic change. References: None - found by reviewing ByteString indexed access under src/main
nvollmar
approved these changes
Sep 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
ByteString.apply(idx)bounds-checks before delegating tobyteAtUnchecked, and for theByteStringscomposite it also resolves the absolute offset to a fragment. Two call sites undersrc/mainpay that check when the caller has already established the bound:ByteStringParser.ByteReader.readByteguards withoff < input.lengthand then callsinput(off), so the bound is tested twice. Its siblingsreadShortLE/readIntLE/readLongLEalready use the*Uncheckedvariants after their own guard, soreadBytewas the odd one out.ByteReaderbacks gzip/deflate header parsing, TLS and framing.TcpDnsClient.decodeLengthread the two length-prefix bytes with twoapplycalls: two bounds checks, and for a compositeByteStringtwo fragment lookups.These are the only two places under
src/mainthat index aByteStringoutsideByteString.scalaitself andbench-jmh.Modification
readBytenow callsinput.byteAtUnchecked(off).offstarts at 0 and only ever moves forward, so the existing guard is sufficient.decodeLengthnow usesdata.readShortBE(0) & 0xFFFF.readShortBEkeeps the bounds check, so short input still throwsIndexOutOfBoundsExceptionas before, and it is SWAR-optimised for the single-arrayByteString1C/ByteString1that the common single-chunk read path produces.Both are internal (
private[pekko]/private[internal]); no public API or binary shape change.Result
One bounds check per byte instead of two in the stream parser, and one checked 16-bit read instead of two indexed reads for the DNS length prefix. Behaviour is unchanged.
Worth noting for reviewers:
ByteStrings.applyis already cheap for sequential access thanks to thefragmentHintmemoisation, so this is a small constant-factor cleanup rather than a complexity fix. I did not switchByteReaderto an iterator — it needsoffas a random-access cursor fortake,sliceandfromStartToHere.Tests
sbt "stream-tests/testOnly org.apache.pekko.stream.io.ByteStringParserSpec org.apache.pekko.stream.scaladsl.CompressionSpec org.apache.pekko.stream.io.compression.*"— 106 succeeded, 0 failedsbt "actor-tests/testOnly org.apache.pekko.io.dns.internal.TcpDnsClientSpec"— 8 succeeded, 0 failedsbt "actor/mimaReportBinaryIssues" "stream/mimaReportBinaryIssues"— successscalafmtrun on the two changed files.References
None - found by reviewing
ByteStringindexed access undersrc/main