Repository navigation
runtime: improve AddCleanup performance #75188
Description
Overview
Meta widely uses CGo, allowing Gophers to access a vast array of C++ libraries.
We previously advocated for the defer Close() pattern for C++ bounded objects; however, this defer call was frequently overlooked, leading to memory leaks as these objects resemble regular Go objects.
Consequently, we adopted automated memory management through runtime.AddCleanup.
Unfortunately, adding these cleanups incurs a performance penalty, specifically during the execution of the runtime.AddCleanup function.
Benchmark
Before implementing this change, we aimed to understand its performance implications.
Several online blog posts compare manual deallocation with runtime.SetFinalizer, highlighting the poor performance of finalizers. We conducted a similar benchmark using the brand new runtime.AddCleanup, introduced in Go 1.24.
While runtime.AddCleanup shows a 2x performance improvement, it still lags behind manual deallocation.
The following benchmark results on Go 1.25.0 on a MacBookPro M1 14" indicate approximately a 5x slowdown compared to manual deallocation (benchmarking on a Linux server yielded similar results):
BenchmarkAllocateFree-8 15169078 73.42 ns/op
BenchmarkAllocateDefer-8 15890224 75.04 ns/op
BenchmarkAllocateAddCleanup-8 3516811 359.7 ns/op
BenchmarkAllocateWithFinalizer-8 1739244 676.9 ns/op
For the benchmark code and additional results, please refer to podtserkovskiy/slow-addcleanup-repro
Further investigation
Delving deeper into this issue, we believe the root cause lies in the linear search within the sorted linked list of special objects on mspan, specifically in the *mspan.specialFindSplicePoint function. A CPU profile generated on Go 1.25rc2 with GOGC=off (for a less noisy picture, as it doesn't significantly affect the results) illustrates this:
Is there a potential to accelerate the *mspan.specialFindSplicePoint function (e.g., by using a skip-list instead of a plain linked-list)?
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTodo
Activity
podtserkovskiy commentedon Aug 29, 2025
cc @mknyszek
[-]runtime: Improve runtime.AddCleanup performance[/-][+]runtime: improve runtime.AddCleanup performance[/+][-]runtime: improve runtime.AddCleanup performance[/-][+]runtime: improve AddCleanup performance[/+]mknyszek commentedon Aug 29, 2025
I have some ideas on how to make this faster, at the cost of some memory overhead. What we brainstormed at GopherCon was essentially:
mspanwhich contains a pointer to the first special for each object.If the size of the array is a problem (as it might be for, say, 8 byte objects) we can also try compressing it. For example, doing a linked list of chunks with some special compact encoding.
The microbenchmark is maximally pessimal, since it forces a full walk of the linked list. In practice I suspect that it's not quite so bad, since the linked list for each span is going to be fairly short.
That being said, it's still not fast. How big are the objects that you're actually applying the cleanup to in production?
podtserkovskiy commentedon Aug 31, 2025
Yeah, I agree the actual impact on the production services isn't studied and nobody reported their services is slow because of this.
The current size is 16 bytes, but it can be reduced (and will be soon to 8 bytes) by swapping fields as we didn't pay a proper attention to alignment/padding.
mknyszek commentedon Sep 2, 2025
Ah sorry, I also meant to also ask: how much total memory does each cleanup represent? Like, how much memory on the C++ side is being cleaned up by each cleanup, would you say, on average? That would help give me a better idea of what the current overheads are.
podtserkovskiy commentedon Sep 4, 2025
I'm trying to get this data. I hope we'll be able to provide it this week.
podtserkovskiy commentedon Sep 9, 2025
I've talked to our language interoperability team. They don't see an easy way to measure it as some C++ classes are pretty basic, but others may refer to arbitrary size of binary data or to other objects via smart pointers.
qmuntal commentedon Sep 14, 2026
The linear scan in
specialFindSplicePointmay be a bottleneck, but per-cleanup allocations also appear to contribute meaningfully toAddCleanup’s overhead.Registration currently allocates both argument storage and a call adapter, adding allocator work and GC pressure on top of maintaining the special list. For the small wrappers described here, that bookkeeping can be significant relative to the actual work. I'll be working on this soon.
gopherbot commentedon Sep 14, 2026
Change https://go.dev/cl/832044 mentions this issue:
runtime: avoid allocating cleanup call adaptersgopherbot commentedon Sep 14, 2026
Change https://go.dev/cl/832025 mentions this issue:
runtime: avoid boxing pointer cleanup argumentsgopherbot commentedon Sep 14, 2026
Change https://go.dev/cl/832124 mentions this issue:
runtime: remove redundant cleanup race synchronizationgopherbot commentedon Sep 14, 2026
Change https://go.dev/cl/832324 mentions this issue:
runtime: speed up special-list insertionqmuntal commentedon Sep 14, 2026
I've sent CL 832324, , targeting the common allocate-then-register pattern used by the reproduction.
A typical use is to allocate a wrapper around a resource and immediately attach its cleanup. Small-object allocation advances through the free slots within a span, so registrations made this way tend to arrive in increasing offset order. The specials list is sorted by offset, but each insertion currently starts searching from the head, even when the previous insertion was immediately before it.
The CL remembers the last inserted special per span. When the next insertion belongs after it, the search can skip that prefix of the list. Otherwise, it falls back to the existing search. The hint is cleared on searches and sweeping so removals cannot leave a stale cached record.
The suggested skip list or per-object index would help more general access patterns, but require more bookkeeping. This approach keeps the existing list and adds just one pointer per span with no growth in individual special records.
With the unchanged reproduction on Windows/ARM64, normal GC, and ten paired samples:
@podtserkovskiy is this enough for your use case? If so, feel free to close this issue.
podtserkovskiy commentedon Sep 21, 2026
Thanks, this looks much better! The goal here was just to highlight the performance difference, not to hit a strict target for
AddCleanup. Happy to close this issue once the CLs merge.Hold a WeakMap's values on its keys, so a value can refer to its key