move readLong, readInt, readShort from ByteStringParser to ByteString so they can be optimized#2847
Conversation
Agent-Logs-Url: https://github.com/pjfanning/incubator-pekko/sessions/0eb96aa0-0ec3-47d6-a26f-732a0367640e Co-authored-by: pjfanning <11783444+pjfanning@users.noreply.github.com>
…read methods Agent-Logs-Url: https://github.com/pjfanning/incubator-pekko/sessions/0eb96aa0-0ec3-47d6-a26f-732a0367640e Co-authored-by: pjfanning <11783444+pjfanning@users.noreply.github.com>
Agent-Logs-Url: https://github.com/pjfanning/incubator-pekko/sessions/0eb96aa0-0ec3-47d6-a26f-732a0367640e Co-authored-by: pjfanning <11783444+pjfanning@users.noreply.github.com>
… usage in SWARUtil Agent-Logs-Url: https://github.com/pjfanning/incubator-pekko/sessions/6c70b3dc-cb53-4f8c-a1e3-692d6e6265c7 Co-authored-by: pjfanning <11783444+pjfanning@users.noreply.github.com>
Agent-Logs-Url: https://github.com/pjfanning/incubator-pekko/sessions/6c70b3dc-cb53-4f8c-a1e3-692d6e6265c7 Co-authored-by: pjfanning <11783444+pjfanning@users.noreply.github.com>
|
@copilot ByteStringParser needs its readShort to be an int return value. Is there a good reason to have SWARUtil support this as well as readShort as a short and accessing this via a new package private unchecked read method in ByteString? Don't add this unless there is a reasonable performance reason to support it. |
…rHandle reads Agent-Logs-Url: https://github.com/pjfanning/incubator-pekko/sessions/db26e214-8a2d-41ef-8720-86247c45c74c Co-authored-by: pjfanning <11783444+pjfanning@users.noreply.github.com>
| override def getShort(implicit byteOrder: ByteOrder): Short = { | ||
| if (len < 2) throw new NoSuchElementException("next on empty iterator") | ||
| if (byteOrder == ByteOrder.BIG_ENDIAN) { | ||
| val result = SWARUtil.getShort(array, from) |
There was a problem hiding this comment.
Might be more explicit passing bigEndian = true as well
There was a problem hiding this comment.
I refactored the methods so ByteOrder is passed explicitly
He-Pin
left a comment
There was a problem hiding this comment.
Overall this PR looks well-structured. A few observations:
Positive:
- Clean separation between bounds-checked public API and unchecked internal methods
- Good test coverage for all three ByteString implementations (ByteString1C, ByteString1, ByteStrings)
- Proper IndexOutOfBoundsException handling with clear error messages
- SWARUtil VarHandle approach with graceful fallback is the right pattern
Minor suggestions:
- The unchecked methods on the abstract
ByteStringclass (lines ~1455-1480) use byte-by-byteapply()calls which is the correct fallback forByteStrings, but consider documenting whyByteStringsdoesn't override these -- it's becauseByteStringscan span multiple chunks making a single VarHandle read unsafe. ByteStringParsercorrectly avoids double-bounds-checking by using*Uncheckedmethods -- good catch.
No blocking issues found.
|
I'll look at making a few of the suggested changes |
| val result = input.readLongBEUnchecked(off) | ||
| off += 8 | ||
| result | ||
| } |
There was a problem hiding this comment.
It would be nice to have some benchmark attached.
There was a problem hiding this comment.
added a benchmark and put results in the description
He-Pin
left a comment
There was a problem hiding this comment.
Deep CR: PR #2847 - move readLong/Int/Short from ByteStringParser to ByteString
Architecture Review
Design Pattern: Template Method with Unchecked Overrides
The pattern of checkReadBounds() + readXxxUnchecked() is well-executed. ByteString1C and ByteString1 override the unchecked methods with SWARUtil VarHandle calls, while ByteStrings falls back to the byte-by-byte default. This is the correct approach since ByteStrings can span multiple internal chunks, making a single VarHandle read unsafe.
However, I'd recommend adding a comment on the abstract readXxxUnchecked methods in ByteString explaining why ByteStrings does not override them. Future maintainers might assume this is an oversight.
Binary Compatibility
New public API methods (readShortBE, readShortLE, readIntBE, readIntLE, readLongBE, readLongLE) are added to the abstract ByteString class with @since 2.0.0. Since Pekko 2.0 is a major version, binary compatibility breaks are expected.
The private[pekko] def readXxxUnchecked(offset: Int) methods are new private[pekko] abstract methods on ByteString. All concrete subclasses must implement them - the PR correctly does this. Since ByteString is sealed abstract class, this is safe.
Performance Considerations
The VarHandle-based multi-byte reads are correct. The .asInstanceOf[Short] cast on shortBeArrayView.get(array, index) is necessary because VarHandle.get() returns Object. The JIT should eliminate this at runtime.
The fallback path (getShortBEWithoutMethodHandle) uses the classic byte-shift approach. Since Pekko 2.0 targets Java 17+, the VarHandle path will always be available - the fallback is defensive coding for exotic JVMs.
Test Coverage
Tests cover all 3 ByteString implementations, both byte orders, boundary conditions, and SWARUtil fallback paths.
Missing: No tests for the ByteIterator.getShort/getInt/getLong overrides. The existing ByteIterator tests should exercise these, but explicit tests would be valuable.
Code Quality
checkReadBoundsusesoffset + size > lengthwhich can overflow for very large offsets. Safe in practice since ByteString length is bounded by Int.MaxValue.ByteStringParserchanges correctly avoid double-bounds-checking by using*Uncheckedmethods.
Java API Coverage
No Java DSL changes needed - these are methods on ByteString (a Scala class) and are naturally callable from Java. The @since 2.0.0 markers and @throws javadoc are properly included.
Suggestions
- Consider adding a comment on the abstract
readXxxUncheckedmethods explaining why ByteStrings does not override them. - Consider adding explicit tests for
ByteIterator.getShort/getInt/getLongoverrides.
| val result = SWARUtil.getShort(array, from) | ||
| from += 2 | ||
| result | ||
| } else if (byteOrder == ByteOrder.LITTLE_ENDIAN) { |
|
@pjfanning btw, I suggest you use copilot cli , which will use less premium requests, and we can use a |
Summary
This PR implements the following changes:
SWARUtil
getShort(array, index, ByteOrder)method using VarHandle byte array views for performance, with fallback implementationsgetShortBEWithoutMethodHandleandgetShortLEWithoutMethodHandleshortBeArrayViewandshortLeArrayViewVarHandle fieldsMethodHandles.byteArrayViewVarHandle(injdk.internal.util.ByteArrayandjdk.internal.util.ByteArrayLittleEndian, backingjava.io.DataInputStreamandjava.util.UUID) and explains the performance benefits: single native load instruction with JIT intrinsification, consolidated bounds check, no alignment requirement, and SWAR arithmeticByteString
readShortBE(offset),readShortLE(offset),readIntBE(offset),readIntLE(offset),readLongBE(offset),readLongLE(offset)public methods to theByteStringabstract classIndexOutOfBoundsExceptionifoffsetis negative or there are insufficient bytes, via a sharedcheckReadBoundshelper@throws[IndexOutOfBoundsException]Javadoc and are marked@since 2.0.0private[pekko]unchecked variants (readShortBEUnchecked,readShortLEUnchecked,readIntBEUnchecked,readIntLEUnchecked,readLongBEUnchecked,readLongLEUnchecked) that skip the bounds check; the public methods delegate to these after checking boundsByteString1CandByteString1override the unchecked variants with direct SWARUtil calls for optimal performance;ByteStringsinherits the byte-by-byte defaultByteStringParser
ByteReader.readShortBE/LE,readIntBE/LE,readLongBE/LEto delegate to the new*UncheckedByteStringmethods, avoiding a redundant bounds check (the reader already guards withNeedMoreData)ByteIterator
Tests
SWARUtilSpec: addedgetShorttests covering both byte orders, VarHandle and fallback pathsByteStringSpec: added correctness tests for all threeByteStringimplementations (ByteString1C,ByteString1,ByteStrings) and bounds-checking tests that verifyIndexOutOfBoundsExceptionis thrown for negative offsets and insufficient dataBenchmark
The Benchmark in this PR shows improved perf for array backed ByteStrings (significant) but possible degradation
for ByteStrings that are concatenated from other ByteStrings (which are not backed by arrays).
I don't have the ideal setup to run perf tests but if anyone has time to run them, that would be great. If anyone has any thoughts about why there might be a slowdown for the
ConcatStringcase, get in touch.With Changes
Without Changes