Feb 15, 2019

[Go] escape analysis discussion

func call(f func()) {
    f()
}

func g() {
    var x int
    
    call(
        func() { x = 1 },  // No escape since 'x' is not used by caller
    )
}

Originally, closures always stored addresses of referenced variables.

At some point an optimization was added that captured variables that are not later modified by the outer function have their values, not their addresses, recorded in the closure.
Please refer to this question on stackoverflow, which is quite interesting due to this side-effect with golang's closure behavior:
https://stackoverflow.com/questions/42162879/mutex-within-loop-leads-to-unexpected-output
package main

import (
    "fmt"
    "sync"
)

func main() {
    mutex := new(sync.Mutex)

    for i := 1; i < 5; i++ {
        for j := 1; j < 5; j++ {
            mutex.Lock()
            go func() {
                fmt.Printf("%d + %d = %d\n", i, j, j+i)
                mutex.Unlock()
            }()
        }
    }
}
---
Result:
1 + 2 = 3
1 + 3 = 4
1 + 4 = 5
2 + 5 = 7
2 + 2 = 4
2 + 3 = 5
2 + 4 = 6
3 + 5 = 8
3 + 2 = 5
3 + 3 = 6
3 + 4 = 7
4 + 5 = 9
4 + 2 = 6
4 + 3 = 7
4 + 4 = 8
---

If not for this optimization, the same problems that force heap allocation above would force heap allocation even for:
func call(f func() int) { 
    f()
}

func g() {
    var x int
    call(func() int { return x })
}

If we tweak that example to modify x after the closure creation, that will disable the closure-value optimization:
func call(f func() int) {
    f()
}

func g() {
    var x int
    call(func() int { return x })  // 'x' escaped to heap since 'x++' from the caller
    x++
}

if the closure itself escapes, then the addresses of the variables are understood to escape too:
func call(f func() *int) *int { 
    f()
    return nil
}

func h() { 
    var y int
    call(
    func() *int { return &y }
    )
}


Or even this:
func h() {
    var y int
    _ = func() *int { return &y }()
}


Both of them decide that &y escapes, and there isn't even a call to analyze in the second.

Golang currently treats values returned by functions (including closures) as escaping to the heap, so there's really no point in worrying that f() might return a value from within the closure.

package p

//go:noinline
func call1(f func() error) error {
 // Leaks *f to result.
 return f()
}

func F1() error {
 y := new(int)
 return call1(func() error {
  y = nil
  return nil
 })
}


//go:noinline
func call2(f func() error) error {
 // No param leakage.
 f()
 return nil
}

func F2() error {
 y := new(int)
 return call2(func() error {
  y = nil
  return nil
 })
}



There's a discussion about interface as function parameter type which causing variable passing in heap allocated.
Reference:
https://www.reddit.com/r/golang/comments/9f9pu8/do_blank_interface_values_escape_to_the_heap/
https://www.reddit.com/r/golang/comments/badeql/golang_memory_escape_analysis_is_naive/
https://stackoverflow.com/a/44699604

The above stackoverflow answer is incorrect, it's not the interface{} being allocated on the heap but the variable it's taken should be allocated on the heap which interface{}'s second pointer will points to it.
(The first pointer will point to type information, or the word will contains the type information, depends on the implement)

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.