Go Runtime Internals

Лекция 12.1

Максим Иванов

Go runtime

2

Compiler

3

Compiler Inlining

package main

import (
    "os"
)

func F(a int) int {
    return 2 / (a - 1)
}

func main() {
    os.Exit(F(len(os.Args)))
}
> go build -o /dev/null -gcflags=-m=1 ./inline/
# gitlab.com/slon/shad-go/lectures/11-runtime/inline
./main.go:7:6: can inline F
./main.go:11:6: can inline main
./main.go:12:11: inlining call to F

Compiler Explorer (godbolt.org) - можно посмотреть ассемблерный код Go

4

Inline и stacktrace

Inline не ломает stacktrace - инлайненные функции видны в стеке:

func inlined() {
    showStack()
}
> go run .
main.showStack
    .../stacktrace/main.go:14
main.inlined          <- функция заинлайнена, но видна в стеке!
    .../stacktrace/main.go:9
main.main
    .../stacktrace/main.go:27
5

Compiler Escape Analysis

package main

var g any

func main() {
    p0 := make([]byte, 10)
    p0[0] = 'f'

    var p1 [10]byte

    copy(p1[:], p0)

    g = p1
}
> go build -o /dev/null -gcflags=-m=2 ./escape/
# gitlab.com/slon/shad-go/lectures/11-runtime/escape
escape/main.go:13:6: p1 escapes to heap:
escape/main.go:13:6:   flow: {heap} = &{storage for p1}:
escape/main.go:13:6:     from p1 (spill) at escape/main.go:13:6
escape/main.go:13:6:     from g = p1 (assign) at escape/main.go:13:4
escape/main.go:6:12: make([]byte, 10) does not escape
escape/main.go:13:6: p1 escapes to heap
6

Compiler Bound Check Elimination

package main

import "encoding/binary"

func main() {
    d := make([]byte, 16)

    // if 10 < len(d) {
    //     panic("out of bound")
    // }
    d[10] = 12

    i := binary.LittleEndian.Uint64(d)
    _ = i
}

func Uint64(b []byte) uint64 {
    _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
    return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |
        uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
}
7

GC

8

GC Finalizer

func openFile() {
    f, err := os.Open("/dev/null")
    if err != nil {
        panic(err)
    }
    // Забыли закрыть файл!
    // Но os.File использует SetFinalizer
    fmt.Printf("Opened file: %v\n", f.Name())
    _ = f
}
9

Finalizer: забыли закрыть файл

Если не закрыть файл явно, finalizer закроет его автоматически:

func openFile() {
    f, err := os.Open("/dev/null")
    if err != nil {
        panic(err)
    }
    // Забыли закрыть файл!
    // Но os.File использует SetFinalizer
    fmt.Printf("Opened file: %v\n", f.Name())
    _ = f
}
> go run noleak.go
Opened file: /dev/null
Opened file: /dev/null
Opened file: /dev/null
Done

Но лучше всегда закрывать явно через defer!

10

GC Finalizer

package main

import (
    "fmt"
    "runtime"
    "time"
)

type myX struct{}

func (xx *myX) close() {
    fmt.Println("finalized")
}

func main() {
    xx := new(myX)
    runtime.SetFinalizer(xx, (*myX).close)

    for {
        time.Sleep(time.Second)
        xx = nil
        runtime.GC()
    }
}
11

Finalizer: SetFinalizer в os.File

os.File использует SetFinalizer для автоматического закрытия:

    // Файл без явного close - закроется через finalizer
    f1, _ := NewFile("/dev/null")
    _ = f1

    // Файл с явным close
    f2, _ := NewFile("/dev/null")
    f2.Close()

    runtime.GC()
    runtime.GC()
> go run with_finalizer.go
Explicit close: /dev/null
Finalizer: closing /dev/null

f2 закрылся явно, f1 закрылся через finalizer

12

Finalizer: отмена при Close

При явном Close() нужно отменить finalizer через `SetFinalizer(f, nil)`:

func (f *File) Close() error {
    // Отменяем финализатор при явном close
    runtime.SetFinalizer(f, nil)
    fmt.Printf("Explicit close: %s\n", f.Name())
    return f.File.Close()
}

Иначе finalizer вызовется повторно и попытается закрыть уже закрытый файл!

13

GC Knobs

В Java ~200+ параметров для настройки GC:
-XX:MaxGCPauseMillis, -XX:GCTimeRatio, -XX:NewRatio, -XX:SurvivorRatio, etc.

В Go всего один основной параметр: GOGC

14

GC Trigger

15

GOGC - что это?

GOGC - процент роста heap'а перед следующей сборкой мусора

Формула: `next_gc = live_heap_size * (1 + GOGC/100)`

16

GOMEMLIMIT

С Go 1.19 добавлен GOMEMLIMIT - мягкий лимит памяти:

Пример: `GOGC=100 GOMEMLIMIT=2GiB ./myapp`

17

GC Sync Pool

var pool = sync.Pool{
    New: func() any {
        return &Decoder{}
    },
}

type Decoder struct {
    buf [1 << 20]byte
}

func New() *Decoder {
    return pool.Get().(*Decoder)
}

func (c *Decoder) Close() {
    pool.Put(c)
}

func handler(w http.ResponseWriter, r *http.Request) {
    d := New()
    d.Close()
}
18

GC Mark & Sweep

19

Concurrent GC

20

GC: Concurrent, не STW!

Go GC работает concurrent* - параллельно с программой, почти без остановок

Старые GC (Java до G1): полная остановка программы на десятки/сотни мс
Go GC: работает параллельно, минимальные паузы

Короткие STW паузы только для:
- Sweep Termination (~10-100 мкс) - финализация предыдущей сборки
- Mark Termination (~100 мкс - 1 мс) - завершение фазы маркировки

99% времени GC работает параллельно с программой!

21

GC: Mark Assist & Write Barrier

*Mark Assist* - горутины помогают GC во время аллокаций:
- Если программа аллоцирует слишком быстро, горутина останавливается
- Помогает GC маркировать объекты чтобы не отстать
- Предотвращает out-of-memory

*Write Barrier* - проверка при записи указателя:
- Отслеживает изменения между объектами во время concurrent GC
- Включается только во время фазы маркировки
- Небольшой overhead (~10-30% на запись указателей)

22

Stack Growth

Go стеки растут динамически:

23

Scheduler

24

Go Scheduler

25

runtime

26

Tools

27

net/http/pprof

func Alloc() {
    for {
        g = make([]byte, 1024*1024)
        time.Sleep(time.Second)
    }
}

func main() {
    for i := 0; i < 10000; i++ {
        go func() {
            select {}
        }()
    }
    go Alloc()
    http.ListenAndServe(":8080", nil)
}
28

pprof model

29

Thank you

Максим Иванов

Use the left and right arrow keys or click the left and right edges of the page to navigate between slides.
(Press 'H' or navigate to hide this message.)