Skip to content

perf(table): fuse 3 allocate calls in Builder.addHelper into 1#2280

Open
shaunpatterson wants to merge 2 commits into
dgraph-io:mainfrom
shaunpatterson:perf/addhelper-fuse-allocate
Open

perf(table): fuse 3 allocate calls in Builder.addHelper into 1#2280
shaunpatterson wants to merge 2 commits into
dgraph-io:mainfrom
shaunpatterson:perf/addhelper-fuse-allocate

Conversation

@shaunpatterson

Copy link
Copy Markdown
Contributor

Summary

Replaces the three separate b.allocate() calls in Builder.addHelper (header, diff-key, value) with a single allocation sized for all three pieces, then writes each piece directly into the slot. The fused write uses *(*header)(unsafe.Pointer(&dst[0])) = h for the header, eliminating the heap-escaping h.Encode() local and two intermediate capacity checks per entry.

Each addHelper call previously paid:

  • 3× capacity check in allocate (one per piece)
  • 1× heap escape for the local 4-byte header slice produced by h.Encode()

After the fuse, that's one capacity check, no heap escape.

Measurement

  • BenchmarkBuilder/no_compression: -12.5%
  • BenchmarkBuilder/zstd_level_1: -13.1%
  • BenchmarkBuilder/encryption: -7.6%
  • Composite (74-benchmark stable subset): -2.28% ns/op, median of 3 runs

Test plan

  • go test -short -race ./table/ — all builder tests pass (TestTableIndex, TestBuilder*, TestTableIterator, ...)
  • go vet ./...
  • CI

addHelper is on the hot path of every table build, so existing tests give 100% line coverage of the modified function.

🤖 Generated with Claude Code

addHelper is the write hot-path: called once per entry during table
build. The original code did three separate writes — append(h.Encode()),
append(diffKey), and allocate+Encode for the value — which costs:
- 3 capacity checks against b.curBlock.data
- 1 heap-escaping local [4]byte from h.Encode() per entry
- 1 intermediate copy of the header bytes

Fuse them into a single allocate(4 + dlen + vsz) and write the header,
diffKey, and value directly into the slice. The header is written via
the same unsafe pointer cast pattern already used elsewhere in the
package (matches header.Encode's byte layout).

Benchmark impact (M4 Max, median of 3 runs):
- BenchmarkBuilder/zstd_compression/level_1: -13.1%
- BenchmarkBuilder/no_compression: -9.4%
- BenchmarkBuilder/encryption: -7.6%
@shaunpatterson shaunpatterson requested a review from a team as a code owner May 23, 2026 18:08
The fused-allocate optimization wrote the block entry header via
`*(*header)(unsafe.Pointer(&dst[0])) = h`, where dst is a sub-slice of
the block buffer at an arbitrary (frequently unaligned) offset. Unlike
header.Encode(), whose make([]byte, headerSize) backing array is
allocator-aligned, this typed store targets an address with no alignment
guarantee. That violates the unsafe alignment rules and can fault
(SIGBUS) or trap into slow emulation on strict-alignment architectures
(32-bit arm, mips, ppc), even though it happens to work on amd64/arm64.

Write the two uint16 header fields with binary.NativeEndian.PutUint16
instead. This reproduces the exact native-endian byte layout that
header.Encode/Decode rely on (so the on-disk format is byte-identical
and round-trips through the unsafe Decode unchanged), is safe at any
offset, and keeps the single-allocate win (no heap-escaping h.Encode()
buffer, one capacity check). Also replaces the hardcoded 4 with the
existing headerSize constant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant