Буфера на стеке горутины

от автора

Задался вопросом размещением массивов и слайсов на стеке, подобно std::array в C++ и массивам в Си. Про стек хорошо написал Vincent Blanchon в статье Go: How Does the Goroutine Stack Size Evolve?. Винсент рассказывает про изменения стека в горутинах. Резюмируя:

  • минимальный размер стека 2 KB;
  • максимальный размер зависит от архитектуры, на 64 битной архитектуре 1 GB;
  • каждый раз размер стека увеличивается в двое;
  • стек как увеличивается, так и уменьшается.

Выясню сколько можно положить на стек и какой оверхед может с собой принести.Буду использовать go 1.13.8 собранный из исходников с отладочной информацией и опции компиляции -gcflags=-m.

Что бы получить отладочный вывод во время работы программы, следует в runtime/stack.go выставить константу stackDebug в нужное значение:

const (     // stackDebug == 0: no logging     //            == 1: logging of per-stack operations     //            == 2: logging of per-frame operations     //            == 3: logging of per-word updates     //            == 4: logging of per-word reads     stackDebug       = 0     //... )

Очень простая программа

Начну с программы которая ничего не делает, что бы посмотреть как выделяется стек под основную горутину:

package main  func main() { }

Видим как заполняется глобальный пул сегментами 2, 8, 32 KB:

$ GOMAXPROCS=1 ./app  stackalloc 32768   allocated 0xc000002000 stackalloc 2048 stackcacherefill order=0   allocated 0xc000036000 stackalloc 32768   allocated 0xc00003a000 stackalloc 8192 stackcacherefill order=2   allocated 0xc000046000 stackalloc 2048   allocated 0xc000036800 stackalloc 2048   allocated 0xc000037000 stackalloc 2048   allocated 0xc000037800 stackalloc 32768   allocated 0xc00004e000 stackalloc 8192   allocated 0xc000048000

Это важный момент. Можем рассчитывать, что будет повторно использоваться выделенная память.

не Очень простая программа

Куда же без Hello world!

package main  import "fmt"  func helloWorld()  {     fmt.Println("Hello world!") }  func main() {     helloWorld() }

runtime: newstack sp=0xc000036348 stack=[0xc000036000, 0xc000036800]     morebuf={pc:0x41463b sp:0xc000036358 lr:0x0}     sched={pc:0x4225b1 sp:0xc000036350 lr:0x0 ctxt:0x0} stackalloc 4096 stackcacherefill order=1   allocated 0xc000060000 copystack gp=0xc000000180 [0xc000036000 0xc000036350 0xc000036800] -> [0xc000060000 0xc000060b50 0xc000061000]/4096 stackfree 0xc000036000 2048 stack grow done stackalloc 2048   allocated 0xc000036000 Hello world!

Размера в 2 KB не хватило и пошло выделение 4 KB блока. Разберемся с цифрами в квадратных скобках. Это адреса, по краям это значение структуры:

// Stack describes a Go execution stack. // The bounds of the stack are exactly [lo, hi), // with no implicit data structures on either side. type stack struct {     lo uintptr     hi uintptr }

Посередине это адрес указывающий на сколько используется стек, вычисляется так:

//... used := old.hi - gp.sched.sp //... print("copystack gp=", gp, " [", hex(old.lo), " ", hex(old.hi-used), " ", hex(old.hi), "]", " -> [", hex(new.lo), " ", hex(new.hi-used), " ", hex(new.hi), "]/", newsize, "\n")

Получаем такую табличку:

Запись Размер Использовано
[0xc000036000 0xc000036350 0xc000036800] 2048 848
[0xc000060000 0xc000060b50 0xc000061000] 4096 2896

Возможно для новых горутин что-то изменится :

package main  import (     "fmt"     "time" )  func helloWorld()  {     fmt.Println("Hello world!") }  func main() {     for i := 0 ; i< 12 ; i++ {         go helloWorld()     }      time.Sleep(5*time.Second) }

$ ./app 2>&1 | grep "alloc 4" stackalloc 4096 stackalloc 4096 stackalloc 4096 stackalloc 4096 stackalloc 4096

Результат такой же, только количество аллокаций меняется от запуска к запуску. Похоже это отрабатывает пул.

Массивы

В stack_test.go используют массивы, поэтому начал с них. Взял простой код и увеличивал bigsize, пока при компиляции go build -gcflags=-m не увидим запись main.go:8:6: moved to heap: x.

package main  const bigsize = 1024*1024*10 const depth = 50  //go:noinline func step(i int) byte {     var x [bigsize]byte     if i != depth {         return x[i] * step(i+1)     } else {         return x[i]     } }  func main() {     step(0) }

Получилось 10 MB, а при depth > 50 заветный stack overflow:

runtime: goroutine stack exceeds 1000000000-byte limit fatal error: stack overflow

Пример использования, для удобства наложил вывод -gcflags="-m"

package main  import (     "fmt"     "os" )  const bigsize = 1024 * 1024 * 10  func readSelf(buff []byte) int { // readSelf buff does not escape     f, err := os.Open("main.go") // inlining call to os.Open     if err != nil {         panic(err)     }      count, _ := f.Read(buff)     if err := f.Close(); err != nil { // inlining call to os.(*File).Close         panic(err)     }      return count }  func printSelf(buff []byte, count int) { // printSelf buff does not escape     tmp := string(buff[:count]) // string(buff[:count]) escapes to heap     // inlining call to fmt.Println     // tmp escapes to heap     // printSelf []interface {} literal does not escape     // io.Writer(os.Stdout) escapes to heap     fmt.Println(tmp) }  func foo() {     var data [bigsize]byte     cnt := readSelf(data[:])     printSelf(data[:], cnt) }  func main() {     foo() // can inline main }

Как отметил Винсент, стек увеличивается в два раза, но есть нюанс:

stackalloc 2048 stackcacherefill order=0   allocated 0xc000042000 runtime: newstack sp=0xc00008ef40 stack=[0xc00008e000, 0xc00008f000]     morebuf={pc:0x48e4e0 sp:0xc00008ef50 lr:0x0}     sched={pc:0x48e4b8 sp:0xc00008ef48 lr:0x0 ctxt:0x0} stackalloc 8192 stackcacherefill order=2   allocated 0xc000078000 copystack gp=0xc000000180 [0xc00008e000 0xc00008ef48 0xc00008f000] -> [0xc000078000 0xc000079f48 0xc00007a000]/8192 stackfree 0xc00008e000 4096 stack grow done ... runtime: newstack sp=0xc0010aff40 stack=[0xc0008b0000, 0xc0010b0000]     morebuf={pc:0x48e4e0 sp:0xc0010aff50 lr:0x0}     sched={pc:0x48e4b8 sp:0xc0010aff48 lr:0x0 ctxt:0x0} stackalloc 16777216   allocated 0xc0010b0000 copystack gp=0xc000000180 [0xc0008b0000 0xc0010aff48 0xc0010b0000] -> [0xc0010b0000 0xc0020aff48 0xc0020b0000]/16777216 stackfree 0xc0008b0000 8388608 stack grow done

Вместо того что бы аллоцировать сразу нужный объем памяти, он будет в цикле увеличивать и копировать стек.

Слайсы

Слайсы до 64 KB могут быть размещены на стеке:

package main  import (     "fmt"     "os" )  const bigsize = 1024*64 - 1  func readSelf(buff []byte) int { // readSelf buff does not escape     f, err := os.Open("main.go") // inlining call to os.Open     if err != nil {         panic(err)     }      count, _ := f.Read(buff)     if err := f.Close(); err != nil { // inlining call to os.(*File).Close         panic(err)     }      return count }  func printSelf(buff []byte, count int) { // printSelf buff does not escape     tmp := string(buff[:count]) // string(buff[:count]) escapes to heap     // inlining call to fmt.Println     // tmp escapes to heap     // printSelf []interface {} literal does not escape     // io.Writer(os.Stdout) escapes to heap     fmt.Println(tmp) }  func foo() {     data := make([]byte, bigsize) // make([]byte, bigsize) does not escape     cnt := readSelf(data)     printSelf(data, cnt) }  func main() { // can inline main     foo() }

stackalloc 131072   allocated 0xc0000d0000 copystack gp=0xc000000180 [0xc0000c0000 0xc0000cff48 0xc0000d0000] -> [0xc0000d0000 0xc0000eff48 0xc0000f0000]/131072 stackfree 0xc0000c0000 65536

Производительность или её отсутствие

В Go переменные инициализируются нулевым значением(zero values). Поэтому ожидамо наличие оверхеда на зануление памяти. Как правильно написать бенчмарк не знаю, поэтому взял интересующий меня кейс и сравню с sync.Pool.

Тестовый кейс:

  • читаем в буфер(из файла);
  • формируем выходной буфер;
  • пишем буфер(в /dev/null).

Да, я знаю, что мерю системные вызовы.

код бенчмарка

func readSelf(buff []byte) int {     f, err := os.Open("bench_test.go")     if err != nil {         panic(err)     }      count, err := f.Read(buff)     if err != nil {         panic(err)     }      if err := f.Close(); err != nil {         panic(err)     }      return count }  func printSelf(buff []byte, count int) {     f, err := os.OpenFile(os.DevNull, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)     if err != nil {         panic(err)     }     if _, err := f.Write(buff[:count]); err != nil {         panic(err)     }      if err := f.Close(); err != nil {         panic(err)     } }  //go:noinline func usePoolBlock4k() {     inputp := pool4blocks.Get().(*[]byte)     outputp := pool4blocks.Get().(*[]byte)      cnt := readSelf(*inputp)     copy(*outputp, *inputp)     printSelf(*outputp, cnt)      pool4blocks.Put(inputp)     pool4blocks.Put(outputp) }  //go:noinline func useStackBlock4k() {     var input [block4k]byte     var output [block4k]byte      cnt := readSelf(input[:])     copy(output[:], input[:])     printSelf(output[:], cnt) }  func BenchmarkPoolBlock4k(b *testing.B) {     runtime.GC()     for i := 0; i < 8; i++ {         data := pool4blocks.Get()         pool4blocks.Put(data)     }     for i := 0; i < 8; i++ {         usePoolBlock4k()     }      b.ResetTimer()      for i := 0; i < b.N; i++ {         usePoolBlock4k()     } }  func BenchmarkStackBlock4k(b *testing.B) {     runtime.GC()     for i := 0; i < 8; i++ {         useStackBlock4k()     }     b.ResetTimer()      for i := 0; i < b.N; i++ {         useStackBlock4k()     } }

$ go test -bench=. -benchmem goos: linux goarch: amd64 pkg: gostack/04_benchmarks BenchmarkPoolBlock4k-12           312918          3804 ns/op         256 B/op          8 allocs/op BenchmarkStackBlock4k-12          313255          3833 ns/op         256 B/op          8 allocs/op BenchmarkPoolBlock8k-12           300796          3980 ns/op         256 B/op          8 allocs/op BenchmarkStackBlock8k-12          294266          4110 ns/op         256 B/op          8 allocs/op BenchmarkPoolBlock16k-12          288734          4138 ns/op         256 B/op          8 allocs/op BenchmarkStackBlock16k-12         269382          4408 ns/op         256 B/op          8 allocs/op BenchmarkPoolBlock32k-12          272139          4407 ns/op         256 B/op          8 allocs/op BenchmarkStackBlock32k-12         240339          4957 ns/op         256 B/op          8 allocs/op PASS

Если у нас два буфера по 4 KB, то разника на уровне погрешности. С увеличением размера блока, увеличивается и разница между пулом и стеком. Взглянем на вывод профилировщика:

(pprof) top 15 Showing nodes accounting for 7.99s, 79.58% of 10.04s total Dropped 104 nodes (cum <= 0.05s) Showing top 15 nodes out of 85       flat  flat%   sum%        cum   cum%      3.11s 30.98% 30.98%      3.38s 33.67%  syscall.Syscall6      2.01s 20.02% 51.00%      2.31s 23.01%  syscall.Syscall      0.83s  8.27% 59.26%      0.83s  8.27%  runtime.epollctl      0.60s  5.98% 65.24%      0.60s  5.98%  runtime.memmove      0.21s  2.09% 67.33%      0.21s  2.09%  runtime.unlock      0.18s  1.79% 69.12%      0.18s  1.79%  runtime.nextFreeFast      0.14s  1.39% 70.52%      0.33s  3.29%  runtime.exitsyscall      0.14s  1.39% 71.91%      0.21s  2.09%  runtime.reentersyscall      0.13s  1.29% 73.21%      0.46s  4.58%  runtime.mallocgc      0.12s  1.20% 74.40%      1.25s 12.45%  gostack/04_benchmarks.useStackBlock32k      0.12s  1.20% 75.60%      0.13s  1.29%  runtime.exitsyscallfast      0.11s  1.10% 76.69%      0.45s  4.48%  runtime.SetFinalizer      0.11s  1.10% 77.79%      0.11s  1.10%  runtime.casgstatus      0.10s     1% 78.78%      0.13s  1.29%  runtime.deferreturn      0.08s   0.8% 79.58%      1.24s 12.35%  gostack/04_benchmarks.useStackBlock16k

Ожидаемо видим syscalls. А теперь на useStackBlock32k и getFromStack32k. Оверхед на массивы составил 120ms:

(pprof) list useStackBlock32k Total: 10.04s ROUTINE ======================== useStackBlock32k in bench_test.go      120ms      1.25s (flat, cum) 12.45% of Total          .          .    158:   printSelf(output[:], cnt)          .          .    159:}          .          .    160:          .          .    161://go:noinline          .          .    162:func useStackBlock32k() {       90ms       90ms    163:   var input [block32k]byte       30ms       30ms    164:   var output [block32k]byte          .          .    165:          .      530ms    166:   cnt := readSelf(input[:])          .      160ms    167:   copy(output[:], input[:])          .      440ms    168:   printSelf(output[:], cnt)          .          .    169:}  ROUTINE ======================== usePoolBlock32k in bench_test.go       10ms      1.18s (flat, cum) 11.75% of Total          .          .    115:   pool16blocks.Put(outputp)          .          .    116:}          .          .    117:          .          .    118://go:noinline          .          .    119:func usePoolBlock32k() {          .       10ms    120:   inputp := pool32blocks.Get().(*[]byte)          .       10ms    121:   outputp := pool32blocks.Get().(*[]byte)          .          .    122:          .      520ms    123:   cnt := readSelf(*inputp)       10ms      200ms    124:   copy(*outputp, *inputp)          .      440ms    125:   printSelf(*outputp, cnt)          .          .    126:          .          .    127:   pool32blocks.Put(inputp)          .          .    128:   pool32blocks.Put(outputp)          .          .    129:}

Заключение:

  • на стеке можно разместить массив объемом 10 MB;
  • размер слайса размещенного на стеке ограничен 64 KB;
  • есть накладные расходы на разделение стека;
  • есть накладные расходы на zero value.

ссылка на оригинал статьи https://habr.com/ru/post/490618/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *