The absurd cost of finalizers in Go
lemire.me
lemire.me
We ran the benchmark here and confirmed the relative times, but our profile shows runtime.addspecial as expected. The expectation is that finalizers are used rarely, for objects where the lifetime is unknown but typically long. In contrast, a finalizer benchmark is hammering on finalizers in a way that real apps basically never do.
There are also some O(N) things in finalizer registration, which turn into O(N^2) things for N finalizers for nearby allocations (only up to N=1024), again because we expect them to be very rare. Those could be fixed if this use case of tons of finalizers is realistic in practice.
If you have locally scoped C data you would typically defer C.free(p), and if you do that the cost is basically identical to calling free(p) directly.
BenchmarkAddition-16 22961580 51.77 ns/op
BenchmarkAllocate-16 7611168 144.3 ns/op
BenchmarkAllocateDeferFree-16 7065505 144.3 ns/op
BenchmarkAllocateFinalizer-16 1028251 1243 ns/op
In response to the "cgo is slow" questions, this shows that cgo calls are about 50ns on my four-year-old x86 MacBook Pro. Is that fast or slow? It depends on what the cgo call is doing. If it's executing a single add instruction, an extra 50ns is slow. If it's doing something more substantial, an extra 50ns may be nothing at all.On the contrary, the final line
> Maybe there is a way to do better, I hope there is, but I suspect not.
to me, says the writer suspects that it isn’t absurd, but as good as it can get.
I also think there’s broad agreement on the fact that finalizers almost always are a bad idea.
For golang, there’s https://crawshaw.io/blog/tragedy-of-finalizers, which says:
“So a common instinct we all have is to say "gee, it sure would be nice if the file descriptor were closed when the File is garbage collected, let's use a finalizer". This does not work.”
and
“It is a bug to depend on the GC to run before your resources are exhausted”
and
“What are finalizers good for? Nothing generally.”
It also says “I believe I have one very small use for finalizers as they exist today, which I will talk about in a followup blog post”. I couldn’t find that post, though.
Java even plans to remove them from the language (https://openjdk.org/jeps/421)
The "Alternative techniques" section of the JEP suggests to use Cleaner.register, which was created as a replacement and has a similar API to Go's runtime.SetFinalizer.
https://docs.oracle.com/en/java/javase/20/docs/api/java.base...
Such code wouldn’t have to mutate the object being finalized (or any other object, for that matter), so I think that use case would better be served by a more light-weight feature, removing finalizers edge cases such as objects that get revived.
I've heard a couple of times that C Bindings in go are slow. Is this what is usually referred to?
So if you're writing a system in both, it's best to have the C side do a lot in each call, vs having the Go side call a lot of tiny funcs.
If you're only calling cgo in a few places, it's not too hard to check each one for memory leaks.