Skip to content

ByteStringBuilder.putInt/putLong: eliminate closure allocation via SWARUtil VarHandle writes#2879

Merged
pjfanning merged 9 commits intoapache:mainfrom
pjfanning:copilot/optimize-bytestringbuilder-fillarray
Apr 23, 2026
Merged

ByteStringBuilder.putInt/putLong: eliminate closure allocation via SWARUtil VarHandle writes#2879
pjfanning merged 9 commits intoapache:mainfrom
pjfanning:copilot/optimize-bytestringbuilder-fillarray

Conversation

@pjfanning
Copy link
Copy Markdown
Member

ByteStringBuilder.putInt and putLong passed lambdas to fillArray, allocating a closure object on every call. This replaces those closures with direct inlined writes using new SWARUtil.putInt/putLong helpers.

Changes

SWARUtil

  • Added putInt(array, index, value, byteOrder) and putLong(array, index, value, byteOrder) — VarHandle set-based writes mirroring the existing getInt/getLong readers
  • Added byte-by-byte fallbacks (putIntBEWithoutMethodHandle, putIntLEWithoutMethodHandle, putLongBEWithoutMethodHandle, putLongLEWithoutMethodHandle) for environments without VarHandle support

ByteStringBuilder

  • putInt and putLong now call ensureTempSize + SWARUtil.putInt/putLong directly, updating _tempLength/_length inline — same structure as fillArray but without the closure
// Before — allocates a lambda on every call
def putInt(x: Int)(implicit byteOrder: ByteOrder): this.type =
  fillArray(4) { (target, offset) => /* byte shifts */ }

// After — no allocation
def putInt(x: Int)(implicit byteOrder: ByteOrder): this.type = {
  ensureTempSize(_tempLength + 4)
  SWARUtil.putInt(_temp, _tempLength, x, byteOrder)
  _tempLength += 4; _length += 4
  this
}

putFloat and putDouble benefit automatically since they delegate to putInt/putLong.

Tests

SWARUtilSpec

  • Added tests for all six new methods (putInt, putIntBEWithoutMethodHandle, putIntLEWithoutMethodHandle, putLong, putLongBEWithoutMethodHandle, putLongLEWithoutMethodHandle) covering specific byte layouts at multiple offsets, boundary values (Int/Long MaxValue/MinValue), offset correctness (surrounding bytes not clobbered), and round-trip consistency with the corresponding get* fallbacks.

ByteStringSpec

  • Added dedicated putInt and putLong test blocks under "A ByteStringBuilder" covering: known byte sequences for both byte orders, boundary values, length assertions, multi-put accumulation, and property-based round-trips against ByteBuffer for both big-endian and little-endian.

Copilot AI and others added 2 commits April 19, 2026 11:37
…SWARUtil VarHandle writes

Agent-Logs-Url: https://github.com/pjfanning/incubator-pekko/sessions/c0a324ab-b053-4254-8d3b-e472278559b9

Co-authored-by: pjfanning <11783444+pjfanning@users.noreply.github.com>
…lder.putInt/putLong

Agent-Logs-Url: https://github.com/pjfanning/incubator-pekko/sessions/d3bbcf71-0623-473b-b9cc-52a23c034666

Co-authored-by: pjfanning <11783444+pjfanning@users.noreply.github.com>
@pjfanning pjfanning marked this pull request as draft April 20, 2026 08:20
@pjfanning
Copy link
Copy Markdown
Member Author

I'll look into creating a JMH benchmark

@He-Pin
Copy link
Copy Markdown
Member

He-Pin commented Apr 20, 2026

nice improvement

@pjfanning
Copy link
Copy Markdown
Member Author

pjfanning commented Apr 20, 2026

With Changes
[info] Benchmark                                Mode  Cnt       Score        Error   Units
[info] ByteString_putInt_Benchmark.putIntBE    thrpt    3  239684.591 ± 102494.674  ops/ms
[info] ByteString_putInt_Benchmark.putIntLE    thrpt    3  272971.614 ±  43025.163  ops/ms
[info] ByteString_putInt_Benchmark.putLongBE   thrpt    3  127419.705 ±   6651.774  ops/ms
[info] ByteString_putInt_Benchmark.putLongLE   thrpt    3  137667.126 ±  25170.882  ops/ms
[info] ByteString_putInt_Benchmark.putShortBE  thrpt    3  266982.432 ±  69828.845  ops/ms
[info] ByteString_putInt_Benchmark.putShortLE  thrpt    3  309618.809 ±   5523.773  ops/ms


Without Changes
[info] Benchmark                                Mode  Cnt       Score       Error   Units
[info] ByteString_putInt_Benchmark.putIntBE    thrpt    3  239961.626 ± 24055.818  ops/ms
[info] ByteString_putInt_Benchmark.putIntLE    thrpt    3  238674.817 ± 47275.478  ops/ms
[info] ByteString_putInt_Benchmark.putLongBE   thrpt    3  131730.867 ±  4642.640  ops/ms
[info] ByteString_putInt_Benchmark.putLongLE   thrpt    3  105625.382 ±  5410.259  ops/ms
[info] ByteString_putInt_Benchmark.putShortBE  thrpt    3  271689.337 ±  7647.398  ops/ms
[info] ByteString_putInt_Benchmark.putShortLE  thrpt    3  260050.052 ± 16497.148  ops/ms

@pjfanning pjfanning marked this pull request as ready for review April 20, 2026 11:46
@pjfanning pjfanning requested a review from He-Pin April 21, 2026 22:31
Comment thread actor/src/main/scala/org/apache/pekko/util/ByteString.scala Outdated
val out = new ByteArrayOutputStream
writeInt(out, snapshotSerializer.identifier)

val ms = migrateManifestIfNecessary(Serializers.manifestFor(snapshotSerializer, snapshot))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SnapshotSerializer changes refactor headerToBinary and the serialization method to use direct byte arrays + SWARUtil.putInt instead of ByteArrayOutputStream + writeInt. This is a significant change to the serialization format logic.

However, no tests are added or modified for SnapshotSerializer to verify that:

  1. The serialized binary format is identical (same byte order, same structure)
  2. Round-trip serialization/deserialization works correctly
  3. The migrateManifestIfNecessary path still produces correct output

Please add tests for SnapshotSerializer to cover these cases, or point to existing tests that would catch regressions here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#2891 has extra tests and they appear in this PR too

if (byteOrder == ByteOrder.BIG_ENDIAN) {
if (intBeArrayViewSupported) {
intBeArrayView.set(array, index, value)
} else {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness concern: The putInt/putLong/putShort methods silently treat any non-BIG_ENDIAN byte order as LITTLE_ENDIAN. The original ByteStringBuilder.putInt threw IllegalArgumentException for unknown byte orders:

} else throw new IllegalArgumentException("Unknown byte order " + byteOrder)

While ByteOrder only has BE/LE instances in practice, this is still a behavioral regression. Consider adding an explicit validation or at least documenting this change.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@OutputTimeUnit(TimeUnit.MILLISECONDS)
@BenchmarkMode(Array(Mode.Throughput))
@Fork(2)
@Warmup(iterations = 5)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance concern: The benchmark only measures the new implementation. There is no baseline @Benchmark method using the old fillArray approach for before/after comparison.

To validate the performance claim (eliminating closure allocation), the benchmark should either:

  1. Include a method that replicates the old fillArray pattern for direct comparison, or
  2. Document that reviewers should run this benchmark against both main and the PR branch.

Without a baseline, the benchmark cannot demonstrate the claimed improvement.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the case with all our benchmarks - it is not hard to run the benchmark with and without the main source changes

Comment thread actor-tests/src/test/scala/org/apache/pekko/util/SWARUtilSpec.scala
Copilot AI and others added 2 commits April 23, 2026 07:42
…Float/putDouble tests, SnapshotSerializer format tests

Agent-Logs-Url: https://github.com/pjfanning/incubator-pekko/sessions/3a10e6a7-4e61-4f22-9ca1-fdbad5891385

Co-authored-by: pjfanning <11783444+pjfanning@users.noreply.github.com>
…utFloat/putDouble and SnapshotSerializer format tests

Agent-Logs-Url: https://github.com/pjfanning/incubator-pekko/sessions/3a10e6a7-4e61-4f22-9ca1-fdbad5891385

Co-authored-by: pjfanning <11783444+pjfanning@users.noreply.github.com>
Copy link
Copy Markdown
Member

@He-Pin He-Pin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@pjfanning pjfanning merged commit bc78cd1 into apache:main Apr 23, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants