stats: extend the lock-free ref-count release fast path to histograms and relax ref-count orderings#46001
Conversation
Releasing a reference to a stat currently takes the allocator's global mutex (the store's histogram lock for histograms) on every release, although the lock is only required when the release can be final: the decrement to zero must be atomic with removing the stat from the by-name set to avoid a destruction/lookup race with concurrent re-allocation of the same stat name (envoyproxy#9533). All stat ref-count atomics are also sequentially consistent, where canonical reference counting needs only relaxed increments and acquire/release decrements. This made the ref-count a flush bottleneck (envoyproxy#43836): MetricSnapshotImpl briefly references every stat, so each flush paid a mutex round-trip per stat on the main thread while dropping those references. Non-final releases now decrement with a CAS loop that can never reach zero outside the lock; a release that observes itself last falls through to the existing locked path unchanged, preserving the resurrection-race protection. RefcountHelper and the stat ref-counts now use the canonical orderings (relaxed increment, release/acq-rel decrement) per the Boost example already cited in the code. bmFlushToSinks (-c opt, aarch64, medians of 5 reps): 1000000 (4M metric objects): 195 ms -> 119 ms (-39%) 2500000 (10M metric objects): 584 ms -> 376 ms (-36%) The 2500000 benchmark case is added by this change; run-to-run variance at 10M objects also drops (CV 21% -> 6%). Partially addresses envoyproxy#43836. Signed-off-by: David Vo <davidvo@lyft.com>
|
Hi @davidvo-lyft, welcome and thank you for your contribution. We will try to review your Pull Request as quickly as possible. In the meantime, please take a look at the contribution guidelines if you have not done so already. |
|
@jmarantz can you take a look at this? |
|
@davidvo-lyft there's a merge conflict; please resolve |
|
@davidvo-lyft I just saw #45821 was merged yesterday, which is working on the same thing. |
|
Thanks for the pointer — I hadn't seen #45821 land (nice speedup, and it independently validates the same fast-path shape). It covers the counter/gauge/text-readout allocator path; the remaining delta in this PR is: the same fast path for histograms ( |
…-fastpath Resolves the allocator.cc conflict in favor of the scalar decRefCount fast path that landed in envoyproxy#45821; this branch keeps the relaxed incRefCount/use_count orderings on top of it. Signed-off-by: David Vo <davidvo@lyft.com>
…roxy#45821 Match the snapshot naming and acq_rel compare-exchange ordering of the scalar-stat fast path that landed in envoyproxy#45821, and point the comment at that implementation, so the two copies stay reviewable side by side. Signed-off-by: David Vo <davidvo@lyft.com>
|
Can you merge main? I think there's a lot left in this PR to add. |
Signed-off-by: David Vo <davidvo@lyft.com>
|
Merged main and resolved the conflict (adopted the #45821 implementation for scalar stats verbatim) and rescoped this PR to the remaining delta: the same fast path for histograms (mirroring the #45821 pattern), relaxed increments + canonical RefcountHelper orderings, multithreaded race tests for the fast path (there are currently only speed tests), and the 10M-object benchmark case. Description updated with fresh same-session numbers against post-#45821 main: a further -13% at 4M metric objects; the 10M case shows no measurable delta on my (busy) workstation — CVs there are 25-31%, so I'd treat it as unresolved rather than zero, and I'm happy to re-run on quiet hardware if useful. |
jmarantz
left a comment
There was a problem hiding this comment.
LGTM; really great, with one question in the code, and this one for the description:
Is the benefit over #45821 for the sink benchmark due to the improvement for histograms, the improvement on incRefCount, or both?
I did a bit of grepping and use_count is used only for tests, afaict. It is used quite a bit for tests though. If you are confident that 'relaxed' reads are OK for that I'm inclined to believe you.
I don't think I've read enough to become expert on the exotic atomic behavior you are exploiting but I did carefully compare wht you did to the boost implementation you used as a reference, and I was very pleased by your analysis of the small changes you made to it, to accomodate TSAN tooling limitations.
@davidvo-lyft up to you if you want to try a small code-sharing refactor; you could do that as a follow-up also. Please let us know your thoughts.
| // the role of the allocator's mutex. | ||
| ASSERT(ref_count_ >= 1); | ||
| uint32_t ref_count_snapshot = ref_count_.load(std::memory_order_relaxed); | ||
| while (ref_count_snapshot > 1) { |
There was a problem hiding this comment.
can we share this block of code somehow, maybe via a helper class, with the counter/gauge mechanism in allocator.cc?
There was a problem hiding this comment.
Done, tryDecRefCountFastPath() in refcount_ptr.h, used by both this and the counter/gauge path. Free function instead of a helper class since the two impls hold their counts differently. The #45821 case-analysis comment moved onto the helper.
Review feedback from jmarantz: extract the CAS fast path introduced in envoyproxy#45821 (and mirrored for histograms here) into tryDecRefCountFastPath() in refcount_ptr.h, used by both StatsSharedImpl::decRefCount and ParentHistogramImpl::decRefCount. A free function over the raw atomic is used rather than a helper class because the two implementations hold their counts differently; the interleaving case analysis moves onto the helper. Signed-off-by: David Vo <davidvo@lyft.com>
Signed-off-by: David Vo <davidvo@lyft.com>
|
Thanks! Ran an interleaved comparison (three configs, same base commit, prebuilt binaries round-robin x3 rounds, aarch64, medians of 5): at 4M metric objects main is 148ms, orderings-only 149ms, full branch 122ms. At 10M the full branch won by 21-22% in every round (405/446/455 vs 318/348/353ms), though single runs at that size are noisy, so call it approximate. So the win is the histogram fast path; the orderings (incl. the relaxed On Code sharing: done in this PR, |
jmarantz
left a comment
There was a problem hiding this comment.
LGTM, thank you!
@ggreenway can you do the final approval?
ggreenway
left a comment
There was a problem hiding this comment.
LGTM; just one nit about changelogs
/wait
| @@ -0,0 +1,7 @@ | |||
| Extended the lock-free non-final reference-count release fast path (added for counters, | |||
There was a problem hiding this comment.
Can you combine this with the previous changelog entry? From the reader/users point of view, it doesn't matter that it was two separate PRs.
There was a problem hiding this comment.
Done. #45821 didn't add a changelog entry, so I reworded this one to describe the combined change from the user's point of view (counters/gauges/text readouts and histograms together, no per-PR split).
Review feedback from ggreenway: describe the whole optimization (the scalar-stat half from envoyproxy#45821 plus this PR) as a single user-facing entry rather than splitting it by PR. Signed-off-by: David Vo <davidvo@lyft.com>
|
/retest |
Rescoped after #45821 landed (which added the same CAS fast path for counters/gauges/text-readouts — this PR originally contained that too; the merge adopts the landed implementation and keeps the remaining, complementary pieces).
What this PR adds on top of main:
ParentHistogramImpl::decRefCountpreviously took the store's histogram lock on every release; it now uses the same pattern that stats: optimize decRefCount to help reduce hot-spot in stats sink #45821 landed for scalar stats. Per review feedback, the CAS fast path is shared by both call sites viatryDecRefCountFastPath()inrefcount_ptr.h(a possibly-final release still falls through to the existing locked path, so the 1->0 transition remains atomic with removal from the by-name set).incRefCountbecomes a relaxedfetch_add(creation/resurrection still happen under the respective locks),RefcountHelpermoves from seq-cst to relaxed-increment /acq_rel-decrement per the Boost example the code already cites (remedy 1 in stats: excessive ref-count inc/dec causing performance bottleneck #43836), anduse_count()becomes a relaxed load (its only non-test caller is theRefcountPtrpassthrough).AllocatorTest.RefCountNonFinalReleaseChurn,AllocatorTest.RefCountCrossThreadFinalRelease(a worker performs the final release while other workers race the fast path), andRefcountPtr.ThreadedCopyDropChurn.server_stats_flush_benchmark(10M metric objects per snapshot — the regime reported in stats: excessive ref-count inc/dec causing performance bottleneck #43836), CI-skipped viaskipExpensiveBenchmarks(), and asource/docs/stats.mdupdate.Benchmarks — interleaved attribution: three configs at the same base commit, prebuilt binaries run round-robin for three rounds (
-c opt, CI docker image, aarch64 = Apple M5 Max / colima 12 vCPU; medians of 5 per round, pooled):Single runs at 10M are noisy (CV 23-39%), but the per-round pairing is consistent. The measured win is the histogram fast path; the orderings don't move this benchmark (their per-op effect shows in #45821's microbenchmarks, not here).
This remains complementary to the direction discussed in #43958/#44031/#44285 (removing the snapshot's per-stat references).
Partially addresses #43836.
Commit Message: stats: extend the lock-free ref-count release fast path to histograms and relax ref-count orderings
Additional Description: see above
Risk Level: Medium (concurrency-sensitive; mirrors the pattern already landed in #45821 for scalar stats; no functional or API changes; final-release locking and lifetime semantics preserved; new multithreaded race tests)
Testing: new multithreaded race tests above plus existing race tests (
RefCountDecAllocRaceOrganic/Synchronized);//test/common/stats/...,//test/server:server_testand//test/server:server_stats_flush_benchmark_testpass locally (clang, opt, aarch64), race tests stressed at--runs_per_test=10on weakly-ordered ARM hardware. Local TSAN is not possible on aarch64 (the prebuilt tsan_libs libc++ is x86_64-only); relying on the CI TSAN job for sanitizer coverage of the lock-free path.Docs Changes:
source/docs/stats.md(lock now required only for final ref-count decrements)Release Notes:
changelogs/current/minor_behavior_changes/stats__lock-free-non-final-ref-count-releases.rstPlatform Specific Features: N/A
Generative AI disclosure: This change was developed with AI assistance (Claude Code). I have reviewed and understand every line, the concurrency reasoning, and the benchmark methodology, and I take full ownership of the submission and of addressing review feedback.