Imagine for a moment there is a kitchen sink with a crack in it. The sink has the potential to leak, but nothing will escape the basin until the sink is used. Much like our imaginary sink, escape analysis inspects variables with the potential to escape when they are used. If that potential exists, the variable is marked as "leaking."
- Criteria: what are the criteria for leaking?
- Leak destination: where does the water go when it goes down the drain?
- Leaking (to a sink): bye-bye
- Leaking to result: you'll be back!
- Leak without escape: you never left!
There are two requirements to be eligible for leaking:
- The variable must be a function parameter
- The variable must be a reference type, ex. channels, interfaces, maps, pointers, slices
Value types such as built-in numeric types, structs, and arrays are not elgible to be leaked. That does not mean they are never placed on the heap, it just means a parameter of int32
is not going to send you running for a mop anytime soon.
If the above criteria is met, then a parameter will leak if:
- The variable is returned from the same function and/or
- is assigned to a sink outside of the stack frame to which the variable belongs.
There are two primary types of leaks:
- leaking (to a sink)
- leaking to result
If a function's parameter is a reference type and the function assigns the parameter to a variable outside of the function, the variable is leaking. While the compiler flag -m
that prints optimizations does not indicate to where the parameter is leaking, it is helpful to think of this as _leaking to a sink. For example:
var sink *int32
func recordID(id *int32) { // leaking param: id
sink = id
}
The function recordID
leaks the parameter id
to the package-level field sink
.
Another type of leak is when a reference parameter is returned from a function:
func validateID(id *int32) *int32 { // leaking param: id to result ~r1 level=0
return id
}
Other than the fact that validateID
has very poor validation logic (indeed some might call it non-existent 😃), the function returns the id
parameter. Because the value of id
is returned, it means it outlives the function's stack frame and has the potential to escape to the heap. Therefore the parameter is marked as leaking to result.
Please remember, a leak is about the potential to escape to the heap. For example, a parameter can leak to result without ever escaping to the heap. For example:
func main() {
var id1 int32 = 4096
if validateID(&id1) == nil {
os.Exit(1)
}
var id2 *int32 = new(int32) // new(int32) does not escape
*id2 = 4096
validID := validateID(id2)
if validID == nil {
os.Exit(1)
}
}
Use the following command to print the optimizations for the above program:
docker run -it --rm go-interface-values \
go tool compile -m ./docs/03-escape-analysis/examples/ex2/main.go
The output should resemble:
./docs/03-escape-analysis/examples/ex2/main.go:22:17: leaking param: id to result ~r0 level=0
./docs/03-escape-analysis/examples/ex2/main.go:32:22: new(int32) does not escape
Please note:
- The
id
parameter for thevalidateID
function is leaking to result because the function returns the incoming parameter and thus there is potential for the value ofid
to outlive its stack frame. - The value
id1
is not even mentioned because it is a value type and escape analysis only applies to reference types. While a pointer toid1
was passed into thevalidateID
function, the Go compiler optimized the pointer to the stack. - The value
id2
is a reference type, but it does not escape. Even though the return value ofvalidateID
is assigned tovalidID
, its object is on the same stack frame asid2
, thus the latter does not outlive its stack frame. Thereforeid2
does not escape to the heap.
A leak is often only felt a drop at a time, but what about a full-on escape?
Next: Escape