19 comments

[ 3.0 ms ] story [ 40.5 ms ] thread
Does anyone know: Is there actually a better way in go?

I've heard a couple of times that C Bindings in go are slow. Is this what is usually referred to?

The cost of calling is slow. The C code itself runs at, well, C speed.

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.

As far as I remember, there's a lot of machinery involved to make a C call because C doesn't know anything about Go's goroutines and their resizable stacks. So the runtime has to prepare the stack accordingly every time you make a call - and it's done on a dedicated CGO thread with a traditional stack.
That’s a misconception that cgo is slow, it’s improving and it’s not slow, when it comes to stacks, Go with its resizable stack and it’s concurrency it’s not hard to say that it’s very much Go is different from other languages. From a normal C function call that returns immediately there’s not much over head BUT when C function takes time with the runtime expecting return value, the runtime will try to align with the C stack creating a more traditional stack, I think there’s a proposal for a directive to let the compiler know there’s no need for the runtime to wait
Isn't it the opposite? cgo calls always have to switch to a larger stack (because there's no way to know in advance how much stack a function will use, and C functions don't support dynamic stack switching like Go functions do), so they have some fixed overhead per call. And that fixed overhead is most significant in relative terms when the call returns quickly.
I stand corrected I don’t know much about Go’s runtime, it’s complicated that following it gives me headache
The better way is to call free manually, instead of relying on a finalizer.

If you're only calling cgo in a few places, it's not too hard to check each one for memory leaks.

There's no space for the finalizer in the object, so it has to be recorded on the side, and it's kinda messy.
There's a good chance you don't want the finaliser in the object; finding finalisers to run would require reading heap memory, when sweeping otherwise just touches side tables (IIUC for the bitmap allocator Go uses).
The benchmarks in the blog post are believable but the profile is not. The benchmarks do roughly the same amount of cgo work; all the extra work in the finalizer benchmark is setting up the finalizer, specifically 'runtime.addspecial'.

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.
Effective Java 2nd Edition is 15 years old now. Is the Java information at all accurate?
The post is about the cost of calling SetFinalizer. Looking at https://github.com/golang/go/blob/master/src/runtime/mfinal.... it seems to be doing a lot, not just storing a function pointer somewhere. There are bunch of validations on the object type and the type of function that is being passed - like it should not be variadic, it should only accept one param and that first param should be of type of the object on which the finalizer is being set.
These are all reasonable things to do at compile time, however for historical reasons since go did not have generics till recently, this is probably the best they can do without compiler specific specialization (eg what was done prior for maps and slices).
I don’t see any argument in this article for the claim of the cost being absurd.

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)

Thanks. So, the small useful feature is that it can be used to (unreliably, as finalizers aren’t guaranteed to run) detect resource leaks.

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.

It's absurd because OP has the wrong understanding of when to use finalizers (as mentioned many times in this thread). Finalizers are not the equivalent of C++ destructors/Rust Drop trait/etc. They're hooks for figuring out when GC is happening.