feat: add ByteString slice-free copyToArray and decodeString overloads - #3464
Open
pjfanning wants to merge 1 commit into
Open
feat: add ByteString slice-free copyToArray and decodeString overloads#3464pjfanning wants to merge 1 commit into
pjfanning wants to merge 1 commit into
Conversation
Two APIs for callers that repeatedly read sub-ranges of a larger buffer, which today have to allocate an intermediate ByteString first. Both were suggested by an analysis of the pekko-http parsers, where the slice-then-use pattern is common in header and line parsing. `copyToArray(srcOffset, dest, destOffset, len)` copies from an offset within this ByteString. The existing overload can only copy from index 0, so copying from an offset requires `drop(n)` or `slice(...)` first, which allocates a new ByteString (and, for a fragmented one, a new fragment vector). `decodeString(charset, from, until)` decodes a sub-range directly. The array-backed layouts pass the offset and length straight to the String constructor, so neither an intermediate ByteString nor an intermediate array is created. Both are implemented on ByteString with a correct generic fallback and overridden in ByteString1C, ByteString1, ByteString2 and ByteStrings so the array-backed layouts use System.arraycopy and the offset-taking String constructor directly. Out of range arguments are clamped rather than throwing, matching the behaviour of the existing copyToArray overloads: the range is reduced to what is available in the source and what fits in the destination, and an empty or inverted range decodes to the empty string. Tests compare both against the slice-then-operate expression they replace, across every internal representation and over all combinations of in and out of range offsets and lengths, including a multi-byte UTF-8 character split across two fragments.
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.
Adds two
ByteStringAPIs for callers that repeatedly read sub-ranges of a larger buffer. Both come from an analysis of the pekko-http parsers, where the slice-then-use pattern is common in header and line parsing.copyToArray(srcOffset, dest, destOffset, len)The existing
copyToArray(dest, destOffset, len)can only copy from index 0, so copying from an offset meansdrop(n)orslice(...)first — which allocates a new ByteString, and for a fragmented one a new fragment vector too.decodeString(charset, from, until)Decodes a sub-range directly. The array-backed layouts pass the offset and length straight to
new String(bytes, offset, len, charset), so neither an intermediate ByteString nor an intermediate array is created.Implementation
Both are defined on
ByteStringwith a correct generic fallback and overridden inByteString1C,ByteString1,ByteString2andByteStrings. The single-array layouts useSystem.arraycopyand the offset-takingStringconstructor;ByteStringslocates the starting fragment and copies whole runs out of consecutive fragments.Out of range arguments are clamped rather than throwing, matching the existing
copyToArrayoverloads: the copy is reduced to what is available in the source and what fits in the destination, and an empty or inverted range decodes to"". This is a deliberate choice for consistency — worth a reviewer's opinion if you would rather they threw.Measurements
Allocation is the reliable signal here and is the point of both APIs. Copying 4096 bytes from offset 2048 of an 8192-byte ByteString:
drop(n).copyToArraycopyToArray(srcOffset, …)Decoding a 45-byte header out of a fragmented buffer:
slice(…).decodeString328 B/op →decodeString(cs, from, until)152 B/op.One case where the new API is slower, which I want to flag rather than bury: with very small fragments (1024 × 8B),
copyToArray(srcOffset, …)runs slower thandrop(n).copyToArray(~15µs vs ~11µs) even though it allocates nothing. The reason is thatdropproduces a ByteString whose existingcopyToArraywalksbytestrings.iteratoronce, whereas the new implementation indexesbytestrings(fragIdx)per fragment, andVector.applyis O(log n). I tried an iterator-based walk; it fixed the timing but reintroduced allocation, which defeats the purpose, so I kept the allocation-free version. 8-byte fragments are also not a realistic shape for the parser use case this targets. Happy to revisit if a reviewer disagrees with that trade.Timings on this machine were noisy run to run; the allocation figures are stable and reproducible, so I would treat the timing numbers as indicative only.
Relationship to the other ByteString PRs
Independent of #3463. I stacked this branch on top of it to check: the two merge cleanly, all tests pass, and the small-fragment gap above persists, because
copyToArraycopies viaArray.copyper fragment and never goes through the memoisedbyteAtUncheckedthat #3463 adds. The one place they do interact is the genericcopyToArrayfallback on the abstract class, which readsbyteAtUncheckedper byte — that path gets faster if #3463 lands, but no built-in layout uses it. Neither PR needs the other, and they can merge in either order.Tests
8 new tests in
ByteStringSpec:slice(...)-based expression they replace, across compacted, sliced, two-fragment and multi-fragment representations, over all combinations of in and out of range offsets and lengthsactor-tests/testOnly org.apache.pekko.util.ByteStringSpecpasses (221 tests).sbt actor/mimaReportBinaryIssuesis clean — both APIs are additive.scalafmtrun on both modules.I did not add a JMH benchmark here, since the win is allocation rather than throughput and the existing ByteString benchmarks are throughput oriented. Happy to add one if preferred.