Skip to content

perf(table): collapse branches in MergeIterator.fix#2284

Open
shaunpatterson wants to merge 1 commit into
dgraph-io:mainfrom
shaunpatterson:perf/merge-fix-collapse-branches
Open

perf(table): collapse branches in MergeIterator.fix#2284
shaunpatterson wants to merge 1 commit into
dgraph-io:mainfrom
shaunpatterson:perf/merge-fix-collapse-branches

Conversation

@shaunpatterson
Copy link
Copy Markdown

Summary

Reworks MergeIterator.fix to:

  1. Hoist the "other" node pointer once. The previous code called mi.bigger() (which does if mi.small == &mi.left { return &mi.right } else { return &mi.left }) up to three times per invocation. Compute it once at the top.

  2. Inline the swap. swapSmall() was a 2-arm conditional that flipped mi.small between &mi.left and &mi.right. With the hoisted big pointer in hand, the swap is just mi.small = big — a straight store, no branch.

  3. Collapse the cmp<0 / cmp>0 cases. The previous switch had two arms whose direction-dependence is symmetric:

    • forward + cmp > 0 → swap to big
    • reverse + cmp < 0 → swap to big
    • otherwise → no-op (small is already correct)

    This is exactly (cmp < 0) == mi.reverse, so we collapse the two arms plus the no-op //nolint:staticcheck arm into a single conditional store.

bigger() and swapSmall() were the only callers of each other and of the inlined logic; with both gone, the helpers become dead code and are removed in the same commit.

Why this is safe

The cmp == 0 arm is unchanged: same-key handling still advances mi.right.next() and then swaps to left if right was small. The two non-equal arms now reduce to: if (cmp < 0) == mi.reverse { mi.small = big }, which is identical to the original switch by case analysis.

Measurement

Composite (74-benchmark stable subset): -0.62% ns/op, median of 3 runs.

Test plan

  • go test -short -race ./table/ — all merge iterator tests pass, including:
    • TestMergeMore (forward/reverse × with/without duplicates)
    • TestMergeIteratorDuplicate (forward/reverse)
    • TestMergeDuplicates (forward/reverse)
    • TestMergeIteratorSeek* (forward/reverse/invalid)
    • TestMergeIteratorNested
  • go vet ./...
  • CI

MergeIterator.fix reaches 100% statement coverage under the existing test suite (verified with -coverprofile).

Note: an earlier draft of an unrelated Next() optimization claimed the other child could not equal curKey after a cmp==0 advance — that claim is false (a child can hold multiple copies of the same user-key with different versions) and was caught by TestMergeDuplicates. This PR does not make that change; it only restructures the branch logic in fix(), preserving the duplicate-handling path verbatim.

🤖 Generated with Claude Code

Rework MergeIterator.fix to:
  - hoist the "other" node pointer once (instead of two bigger()
    calls), avoiding repeated &mi.left/&mi.right comparisons
  - inline the swap (mi.small = big) where it was previously hidden
    behind swapSmall(), so the compiler sees a straight pointer
    assignment with no branch
  - collapse the cmp<0 / cmp>0 cases into a single conditional swap
    expressed as `(cmp < 0) == mi.reverse`, removing one switch arm
    plus the no-op staticcheck-suppressed branch

This makes the hot per-step decision in iteration two compares and
one conditional store, instead of a 4-arm switch with embedded
helper-method dispatches. y.CompareKeys is now the only call site
for the LCP comparison, leaving direction handling fully in fix().

bigger() and swapSmall() were the only callers of each other and of
the inlined logic; with both gone, the helpers become dead code and
are removed in the same commit.

Existing merge iterator tests (TestMergeMore, TestMergeIteratorSeek,
TestMergeIteratorDuplicate, TestMergeDuplicates — forward + reverse
variants) cover this path at 100% statement coverage. Measured
-0.62% composite ns/op across the 74-benchmark stable subset.
@shaunpatterson shaunpatterson requested a review from a team as a code owner May 23, 2026 18:09
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