Skip to content

Keep Get's hit path free of helper calls - #1

Open
JohanLindvall wants to merge 1 commit into
mainfrom
inline-get-hit-path
Open

JohanLindvall wants to merge 1 commit into
mainfrom
inline-get-hit-path

Conversation

@JohanLindvall

Copy link
Copy Markdown
Owner

Both Gets went through factory.reuse, which did the reset check and the miss-path build in one function. That costs 149 against the inliner's budget of 80, so every Get paid an extra call frame, hit or miss. The typed-pool wrapper in Lumberjack that objpool was extracted from did not: its Get branched on resetFn itself and only called out to build on a miss.

Change

  • factory.reuse is split into factory.rewind (the reset check, cost 69) and factory.build (the miss path, cost 76). The element contract still lives once in factory.
  • Pool.Get and FreeList.Get test rewind in its own if, nested under the nil check. Joined with &&, the compiler materializes rewind's result as a bool (SETE + TESTL) on the hit path. Isolated by building both half-variants: the && in Get causes it, not the || inside rewind.
  • CLAUDE.md gains one design-decision bullet so neither regression comes back.

Inlining evidence

go test -c -gcflags=-m=2 in a consumer module (generic methods report inlining only where they are instantiated):

before: cannot inline objpool.(*factory[SHAPE]).reuse: function too complex: cost 149 exceeds budget 80
after:  can inline objpool.(*factory[SHAPE]).rewind with cost 69
        can inline objpool.(*factory[SHAPE]).build with cost 76
        pool.go:61:22: inlining call to objpool.(*factory[SHAPE]).rewind
        pool.go:65:24: inlining call to objpool.(*factory[SHAPE]).build
        freelist.go:72:22: inlining call to objpool.(*factory[SHAPE]).rewind
        freelist.go:76:24: inlining call to objpool.(*factory[SHAPE]).build

No factory helper symbol remains in the consumer binary, and Pool.Get's hit path is sync.Pool.Get → type check → resetFn → return. The one SETE left in FreeList.Get is sync.Mutex.Lock's inlined CAS, which is also present before.

Benchmarks

amd64 (Ultra 9 185H), Go 1.27.1. Variants run interleaved and back to back with the order reversed every other round; benchstat.

Isolated Get+Put hit path, from a scratch module that is not part of this PR; n=16:

v0.0.1 this PR
Pool, no reset 9.937 ns 9.235 ns -7.06% (p=0.008)
Pool, reset 10.55 ns 10.05 ns -4.7%, not significant (p=0.060)
FreeList, reset 21.80 ns 21.12 ns -3.10% (p=0.002)
FreeList, no reset 21.21 ns 21.49 ns not significant (p=0.220)

Real-world, Lumberjack's per-line sampler, whose Pool has a reset; n=20:

Benchmark_Sampler_rate ns/op
Lumberjack's pre-extraction pkg/pool 38.82
objpool v0.0.1 40.26 +3.71% vs pkg/pool (p=0.000)
this PR 39.34 -2.29% vs v0.0.1 (p=0.007); vs pkg/pool not significant (p=0.066)

Benchmark_DynamicMetrics_Add_ProdShape is flat (1.004 µs both, p=0.723). Allocations are unchanged everywhere, and zero on every hit path.

Checks

gofmt, go vet, go test -race -shuffle=on at 100.0% coverage, golangci-lint v2.13.1: 0 issues.

🤖 Generated with Claude Code

Both Gets went through factory.reuse, which did the reset check and the
miss-path build in one function. Together they cost 149 against the
inliner's budget of 80, so every Get paid a call frame, hit or miss.

Split it into factory.rewind (the reset check, cost 69) and factory.build
(the miss path, cost 76). Both inline, so a hit now calls nothing beyond
sync.Pool.Get (or the mutex) and resetFn, and the contract still lives in
factory. Get tests rewind in its own if, nested under the nil check:
joined with &&, the compiler materializes the result as a bool
(SETE/TEST) on the hit path.

Measured interleaved: Pool Get+Put without a reset -7.1%, FreeList with
a reset -3.1%, and Lumberjack's Benchmark_Sampler_rate -2.3%
(40.26 -> 39.34 ns), within noise of its pre-extraction pool.

Co-Authored-By: Claude Opus 5.5 (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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant