Daniel Lemire's blog

, 18 min read

The absurd cost of finalizers in Go

26 thoughts on “The absurd cost of finalizers in Go”

  1. Omari Omarov says:

    I’m not sure about Go’s GC, but in .NET and Java finalizers are run during GC. No CPU cycles are spent before that.

    1. In never incarnations of Java finalizers may not even be called. Finalizers should be replaced with try-finally or AutoClosables. It’s really a bad idea to depend on finalizers.

      1. What is the alternative?

        1. Ben Manes says:

          Phantom references, e.g. by using Cleaner. Finalizers are deprecated for removal in Java.

        2. Yawar says:

          In Go? io.Closer and defer. Or at least just defer. E.g.:

          func whatever() {
          c := C.allocate()
          defer C.free_allocated(c)

          // my code
          } // c is deallocated

          1. gc says:

            your sample only works for block-scoped lifetimes.
            it is insufficient for allocating functions.

  2. Antoine says:

    Have you tried measuring a dummy finalizer that does not call into C? The title says “the absurd cost of finalizers” but, judging by your profiling results, it might be the absurd code of CGo function calls (CGo is well-known to be slow, though by how much I don’t know).

    1. Blog post updated: it is really the finalizer.

      1. Jens Alfke says:

        I don’t see anything about sanitizers in the post?

        1. Finalizer, not sanitizer.

  3. User says:

    Go finalizers are not guaranteed to be run when a value is GC’d.

    1. As long as the process terminates, then it is fine.

  4. Stuart Marks says:

    How are finalizers run if garbage collection is disabled? If Go is anything like Java, then finalizers are called by GC when it determines the object is no longer in use. The Go docs seem to back this up. (But I’m not a Go expert so something else may be going on.)

    https://pkg.go.dev/runtime#SetFinalizer

    1. It is setting up the finalizer that is expensive. The garbage collection is not the issue.

      1. Stuart Marks says:

        Ah, I see, you are benchmarking only the setup of the finalizer, not waiting for the object’s finalizer to be called. Yes that overhead is surprising. I believe that the scenario from Effective Java includes includes time waiting for finalizer to be called, which includes GC latency, so it isn’t directly comparable.

  5. Karl Meissner says:

    In Go, a typical pattern is

    r := createMyResource()
    // could check errors if thats a thing for r
    defer closeMyResource(r)
    // use the resource r without fears of leaks for the scope of the function

    Defer called used to be expensive but they were optimized a few years back. It would interesting to see how they compare to finalizers for your use case. I expect they would be more expensive then the manual call but safer in the face of panics or multiple return points in the using code.

    1. I know about defers, but they are not functionally equivalent because I cannot force the caller to use a defer in Go.

      1. Alexey Sharov says:

        Big part of stdlib in go already designed to be used in “defer close()” pattern: file.Close(), bufio.Flush(), httpResponse.Body.Close(), mutex.Unlock(), close(chanel), … So, devs getting use to it anyway.
        Can’t force caller to check error returned by your function. Doesn’t mean it’s “not functional equivalent for exceptions” – it’s “enough equivalent”. And all go devs know what to do with “err” (even if beginners doing mistakes). I think “if err != nil” is conceptually same thing with “defer close()” – no compiler guaranties, need manual work, fine.
        We force devs to add “defers dbTransaction.Rollback()” by custom linter: https://github.com/ledgerwatch/erigon/blob/devel/rules.go#L31

  6. Chris says:

    In Go, a typical pattern is
    this was also my first thought. Why not just defer C.free_allocated(c)?

    I cannot force the caller to use a defer in Go
    can you maybe give an example where this is a problem?

  7. Chris says:

    In Go, a typical pattern is … defer

    this was also my first thought. Why not just defer C.free_allocated(c)?

    I cannot force the caller to use a defer in Go

    can you maybe give an example where this is a problem?

    1. Sure.

      Looks at this library:

      https://github.com/ada-url/goada

      It returns a URL parsed from C.

      Here is another library which returns a bitmap, stored in C:

      https://github.com/RoaringBitmap/gocroaring

      In both cases, the Go version can be much slower than the Python equivalent when creating new values. By much, I mean, several times (at least two times).

  8. The Alchemist says:

    For some content, Java’s Object.finalize() has been deprecated since ~Java 9, 2018, and not recommended for usage for years before then. It was removed entirely ~last year in Java 18. Performance and non-determinism are just reasons why.

    There’s a very detailed description of this issue at https://openjdk.org/jeps/421, not just from a Java perspective but a more generic VM and GC perspective.

    For Java specifically, though, try-with-resources and java.lang.ref.Cleaner‘s are two of the recommendations for replacement.

  9. me says:

    The quotes Java measurement likely is apples and oranges, similar may apply to the Go code.
    Such languages live from inlining small objects to lessen the cost of memory management.
    Very likely the use of finalizers kills the inline ability (because one does not really need finalizes in cases where small obejects can be inlined), as they will be put in a memory management queue.
    IMHO a more realistic benchmark would be to allocate a list of 1000 such objects, then free them, so you don’t measure “boxing + finalizer” when you want to measure finalizers only.

  10. RICHARD HUDSON says:

    Finalizers should be the last option. If a design finds itself reaching for finalizers then perhaps the design needs rethought to use defer or to avoid using C to manage resources. I’m not saying finalizers (or something similar) aren’t needed sometimes but as the blog points out there is a cost.

  11. jerch says:

    (Slightly offtopic)

    Imho this illustrates a more fundamental issue we have bought into with high level programming languages – while they allow more easy/convenient programming by abstracting many goodies, these goodies come to a price and increase the distance to the machine and a proper comprehension of its real work. Or to say it differently – if Go offers a finalizer, it will get used, no matter how bad it is.

    To illustrate that further, imho the broad adoption of GCs into languages led to a programming style, where devs dont care much about memory anymore, resulting in overly moving data around (e.g. copy constructors, hire&forget memory).

    There was a paper from google years ago stating that memcopy actions account for a very high percentage of their server load (dont remember the exact numbers anymore, I think it was >30%). Imho this will only get worse with the ongoing broader adoption of languages, that heavily rely on that hire&forget memory model, like Javascript/NodeJS and Python.

  12. Christoph says:

    The article “Some notes on the cost of Go finalizers (in Go 1.20)” by Chris Siebenmann digs a bit deeper into the behavior of the finalizer.