> ## Content Index
> Fetch the complete content index at: https://huizhou92.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Go High-Performance Programming EP9: Escape Analysis
- URL: https://huizhou92.com/go-high-performance-programming-ep9-escape-analysis-2/
- Published: 2024-08-15T20:04:40.000Z
- Updated: 2026-09-08T02:30:43.000Z
- Description: Go High-Performance Programming EP9: Escape Analysis. From the perspective of the Go compiler, memory allocation happens in two places: the stack and the h。
- Author: huizhou92
- Tags: #Migrated-1788833207488, #Import 2026-09-08 02:07

From the perspective of the Go compiler, memory allocation happens in two places: the `stack` and the `heap`. This difference doesn't matter much for most developers, as the Go compiler handles memory allocation automatically. However, from a performance standpoint, there is a significant difference between allocating memory on the stack versus the heap. If memory is allocated on the stack within a function, it is automatically reclaimed when the function completes. If it's allocated on the heap, it's managed by the garbage collector (GC), which involves a more complex process and can introduce performance overhead. This phenomenon of allocating memory on the heap when it could be on the stack is known as **escape**. When we write code, we should try to avoid heap memory allocation.

### Why Does Escape Happen?

The primary reasons for memory escape are simple: the compiler cannot determine the variable’s lifetime, or the stack does not have enough space for the required memory. The Go compiler uses **escape analysis** to decide whether a variable should be allocated on the stack or the heap. This process happens during the compilation stage and can be observed using the `-gcflags=-m` command to determine if a variable escapes to the heap.

### Impact of Escape on Performance

A straightforward benchmark test illustrates the impact of memory escaping. In the example below, `BenchmarkInt` uses a pointer array, causing escape with `&j`, whereas `BenchmarkInt2` does not cause `escape`.

```go
func BenchmarkInt(b *testing.B) {   
     for i := 0; i < b.N; i++ {   
         a := make([]*int, 100)   
         for j := 0; j < 100; j++ {   
             a[j] = &j   
         }   
     }   
 }   
    
 func BenchmarkInt2(b *testing.B) {   
     for i := 0; i < b.N; i++ {   
         a := make([]int, 100)   
         for j := 0; j < 100; j++ {   
             a[j] = j   
         }   
     }   
 }
```

![](https://huizhou92.com/content/images/2026/09/0-uc2fzg6iyv4raojo.png)

Compare heap & stack perf

The results show that `BenchmarkInt2` is 30 times faster than, with no memory allocations, whereas `BenchmarkInt` has 101 memory allocations. This test highlights the importance of conducting escape analysis.

### Common Scenarios of Memory Escaping

#### Variable and Pointer Escaping

When a variable’s lifecycle extends beyond the function scope, the compiler allocates it on the heap, leading to **memory variable escape** or **pointer escape**.

```go
package main 
 ​ 
 import "fmt" 
 ​ 
 func main() { 
     s := makeString() 
     fmt.Println(s) 
 } 
 ​ 
 func makeString() *string { 
     str := "Hello, World!" 
     return &str 
 }
```

![](https://huizhou92.com/content/images/2026/09/0-4jmo4zjerwm0moy7.png)

#### Stack Overflow

The operating system limits the stack size used by kernel threads; on a 64-bit system, this limit is typically 8 MB. You can check the maximum allowed stack size with the `ulimit -a` command. The Go runtime dynamically allocates stack space as needed for goroutines, starting with an initial size of 2 KB. If local variables exceed a certain size, typically 64 KB, they escape to the heap. For instance, attempting to create an array of 8193\*8 bytes:

```go
func main() {   
     a := make([]int64, 8176)   
     b := make([]int64, 8192)   
     c := make([]int64, 8193)   
     println(a, b, c)   
 }
```

Arrays larger than 8192\*8 bytes escape to the heap.

![](https://huizhou92.com/content/images/2026/09/0-fcz3mvtvdgdxottx.png)

#### Variables with Unknown Size

```go
func main() {   
     a := generate(3)   
     println(a)   
 }   
 ​ 
 func generate(n int) []int {   
     return make([]int, n)   
 }
```

Since the size of `generate`'s parameter is determined at runtime, the compiler cannot allocate it on the stack, causing it to escape to the heap.

![](https://huizhou92.com/content/images/2026/09/0-y3lzjyldm632c2c_.png)

#### `interface{}` Dynamic Type Escape

In Go, the empty interface `interface{}` can represent any type. If a function parameter is `interface{}`, the compiler cannot determine its type during compilation, leading to memory escape.

```css
func main() {   
     v := "Hello,World"   
     fmt.Printf("addr of v in bar = %p\n", &v)   
 }
```

![](https://huizhou92.com/content/images/2026/09/0-sficjvgac4ohz3bu.png)

Since `fmt.Printf` accepts parameters of type `any` (equivalent to `interface{}`), `v` escapes to the heap.

#### Closure

In the following example, the function `Increase()` returns a closure that accesses the external variable `n`. Thus, `n` persists beyond the function's scope, causing it to escape to the heap.

```go
func Increase() func() int {   
     n := 0   
     return func() int {   
         n++   
         return n   
     }   
 }   
    
 func main() {   
     in := Increase()   
     fmt.Println(in()) // 1   
     fmt.Println(in()) // 2   
 }
```

### Manually Avoiding Escapes

In the **`interface{}` Dynamic Type Escape** example, memory escape occurs even when simply printing "Hello, World." To prevent `v` from escaping, we can use the following function from the Go runtime code:

```csharp
// $GOROOT/src/runtime/stubs.go 
 func noescape(p unsafe.Pointer) unsafe.Pointer { 
     x := uintptr(p) 
     return unsafe.Pointer(x ^ 0)  
 }
```

This function, widely used in the Go standard library and runtime, converts a pointer to a value and then back to a pointer, breaking the escape analysis data flow and preventing the pointer from escaping.

![](https://huizhou92.com/content/images/2026/09/0-lgytch0rsxqth1k2.png)

```css
func noescape(p unsafe.Pointer) unsafe.Pointer {   
     x := uintptr(p)   
     return unsafe.Pointer(x ^ 0)   
 }   
    
 func main() {   
     v := "Hello,World"   
     v2 := "Hello,World1"   
     fmt.Printf("addr of v in bar = %p \n", (*int)(noescape(unsafe.Pointer(&v))))   
     fmt.Printf("addr of v in bar = %p\n", &v2)  
 }
```

The output shows that `v` does not escape while `v2` does.

![](https://huizhou92.com/content/images/2026/09/0-vcogwbskxhg3whst.png)

### Conclusion

In typical development scenarios, memory escape analysis is rarely a concern. Based on experience, optimizing a lock can yield better performance improvements than conducting multiple memory escape analyses. During regular development, remember:

- Passing values copies the entire object, while passing a pointer only copies the pointer address, pointing to the same object. Passing a pointer can reduce value copying but may lead to memory allocation escaping to the heap, increasing the garbage collection (GC) burden. In frequent object creation and deletion scenarios, the GC overhead from passing pointers can significantly impact performance.

**Generally, you should choose to pass a pointer for modifying original object values or for larger struct objects. Passing values offer better performance for read-only, smaller struct objects.**

### References

- [Five Things That Make Go Fast](https://dave.cheney.net/2014/06/07/five-things-that-make-go-fast?ref=huizhou92.com)
- [ICSE20 Paper](http://www.wingtecher.com/themes/WingTecherResearch/assets/papers/ICSE20.pdf?ref=huizhou92.com)
- [Understanding Go Escape Analysis by Example](https://tonybai.com/2021/05/24/understand-go-escape-analysis-by-example/?ref=huizhou92.com)
- [Go’s Hidden Pragmas](https://dave.cheney.net/2018/01/08/gos-hidden-pragmas?ref=huizhou92.com)

[文章索引](/article-index/)