Skip to content

GODRIVER-4018 Reduce allocations when building write batches#2476

Open
paveon wants to merge 1 commit into
mongodb:masterfrom
paveon:godriver-appendbatchsequence-pregrow
Open

GODRIVER-4018 Reduce allocations when building write batches#2476
paveon wants to merge 1 commit into
mongodb:masterfrom
paveon:godriver-appendbatchsequence-pregrow

Conversation

@paveon

@paveon paveon commented Jul 9, 2026

Copy link
Copy Markdown

GODRIVER-4018

Summary

Pre-size the output buffer once in Batches.AppendBatchSequence and
AppendBatchArray instead of letting the per-document append grow it by
repeated reallocation. A first pass totals the documents that fit within the
maxCount/totalSize limits, dst is grown a single time, then the documents
are appended; when dst already has capacity the reservation is a no-op.
AppendBatchArray also now writes each element's numeric key in place with
strconv.AppendInt rather than allocating a string via strconv.Itoa.

Behavior is unchanged: same number of batches appended, byte-identical output,
same limit handling, and the same empty-result early return.

Background & Motivation

Both methods build the write payload for write operations (BulkWrite,
InsertMany, etc.) by appending each encoded document to dst in a loop with no
pre-sizing, so append grows dst geometrically — each growth reallocates and
copies the whole buffer accumulated so far. On large batches this is significant,
avoidable allocation churn and GC pressure, and it showed up as a top allocation
site in a profile of a write-heavy service. Document sizes are known up front, so
the capacity can be reserved in one shot; the limit-checking logic is unchanged.
The array path additionally allocated one key string per document. Minimum Go is
1.19, so slices.Grow is not used.

Benchmark (1000 × 256-byte documents, appending into a fresh buffer):

Function allocs/op B/op ns/op
AppendBatchSequence 24 → 3 1,186,945 → 262,169 ~130,000 → ~29,000
AppendBatchArray 925 → 3 1,189,861 → 262,169 ~166,000 → ~38,000

@paveon paveon requested a review from a team as a code owner July 9, 2026 20:05
@paveon paveon requested a review from prestonvasquez July 9, 2026 20:05
Reserve the output buffer once before appending documents in
AppendBatchSequence and AppendBatchArray, instead of letting the
per-document append grow it by repeated reallocation. A first pass
computes how many documents fit within the maxCount/totalSize limits and
their combined size; dst is grown a single time, then the documents are
appended. When dst already has capacity the reservation is a no-op.

AppendBatchArray additionally wrote each element key with strconv.Itoa,
allocating a string per document; it now writes the numeric key in place
with strconv.AppendInt via a helper that composes the same bsoncore
primitives, producing byte-identical output.

Behavior is unchanged: same batch count, byte-identical output, same
maxCount/totalSize handling, and the same empty-result early return.

Benchmark (1000 x 256-byte docs, fresh buffer):
  AppendBatchSequence: 24 -> 3 allocs/op, 1.19MB -> 262KB, ~130us -> ~29us
  AppendBatchArray:    925 -> 3 allocs/op, 1.19MB -> 262KB, ~166us -> ~38us
@paveon paveon force-pushed the godriver-appendbatchsequence-pregrow branch from d277f19 to d6bf08d Compare July 9, 2026 20:15
@prestonvasquez prestonvasquez removed their request for review July 13, 2026 18:35
@matthewdale

Copy link
Copy Markdown
Contributor

@paveon Thanks for the improvement! The Go Driver team is at capacity right now, but we've scheduled reviewing your PR after some pressing work. Someone from the team will review it in about a month.

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.

2 participants