Go Runtime Internals
Лекция 12.1
Максим Иванов
Максим Иванов
-m=1 -m=2package 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
4Inline не ломает stacktrace - инлайненные функции видны в стеке:
func inlined() { showStack() }
> go run .
main.showStack
.../stacktrace/main.go:14
main.inlined <- функция заинлайнена, но видна в стеке!
.../stacktrace/main.go:9
main.main
.../stacktrace/main.go:27package 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 heappackage 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 }
GOGCfunc openFile() { f, err := os.Open("/dev/null") if err != nil { panic(err) } // Забыли закрыть файл! // Но os.File использует SetFinalizer fmt.Printf("Opened file: %v\n", f.Name()) _ = f }
Если не закрыть файл явно, 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!
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() } }
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
При явном 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
В Java ~200+ параметров для настройки GC:
-XX:MaxGCPauseMillis, -XX:GCTimeRatio, -XX:NewRatio, -XX:SurvivorRatio, etc.
В Go всего один основной параметр: GOGC
GOGC - процент роста heap'а перед следующей сборкой мусора
GOGC=100 (по умолчанию) - GC запускается когда heap вырос в 2 разаGOGC=200 - GC запускается когда heap вырос в 3 разаGOGC=50 - GC запускается когда heap вырос в 1.5 разаGOGC=off - отключить GC (только для тестов!)Формула: `next_gc = live_heap_size * (1 + GOGC/100)`
16
С Go 1.19 добавлен GOMEMLIMIT - мягкий лимит памяти:
GOMEMLIMIT=1GiB - Go попытается не превышать 1GBПример: `GOGC=100 GOMEMLIMIT=2GiB ./myapp`
17var 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() }
Go GC работает concurrent* - параллельно с программой, почти без остановок
Старые GC (Java до G1): полная остановка программы на десятки/сотни мс
Go GC: работает параллельно, минимальные паузы
Короткие STW паузы только для:
- Sweep Termination (~10-100 мкс) - финализация предыдущей сборки
- Mark Termination (~100 мкс - 1 мс) - завершение фазы маркировки
99% времени GC работает параллельно с программой!
21
*Mark Assist* - горутины помогают GC во время аллокаций:
- Если программа аллоцирует слишком быстро, горутина останавливается
- Помогает GC маркировать объекты чтобы не отстать
- Предотвращает out-of-memory
*Write Barrier* - проверка при записи указателя:
- Отслеживает изменения между объектами во время concurrent GC
- Включается только во время фазы маркировки
- Небольшой overhead (~10-30% на запись указателей)
Go стеки растут динамически:
GOMAXPROCS
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) }
Максим Иванов