Советы Golang по оптимизации производительности GC

Go

слайс предварительно выделяет память

когдаsliceПри емкости менее 1024 емкость увеличивается в 2 раза. Когда емкость превышает 1024, увеличенная емкость в 1,25 раза превышает исходную. См. следующий пример:

func appendOne(num int) []int {
	var res []int
	for i := 0; i < num; i++ {
		res = append(res, i)
	}
	return res
}

func appendMany(num int) []int {
	res := make([]int, 0, num)
	for i := 0; i < num; i++ {
		res = append(res, i)
	}
	return res
}

функцияappendOneНачальный размер емкости не указан,appendManyЗадает начальный размер емкости. Проведите контрольный тест:

func BenchmarkAppendOne(b *testing.B) {
	num := 10000
	for i := 0; i < b.N; i++ {
		_ = appendOne(num)
	}
}

func BenchmarkAppendMany(b *testing.B) {
	num := 10000
	for i := 0; i < b.N; i++ {
		_ = appendMany(num)
	}
}

запустить тест

$ go test -bench=. -benchmem                                                      
goos: darwin
goarch: amd64
pkg: com.learn/gormLearn/gc_gc
BenchmarkAppendOne-4               23163             50675 ns/op          386296 B/op         20 allocs/op
BenchmarkAppendMany-4              96781             12241 ns/op           81920 B/op          1 allocs/op
PASS

можно увидеть,AppendMany1 выделение памяти выполняется для каждой операции, и каждое выделение памяти выделяется81920 B, каждая операция занимает время12241 ns, эти показатели лучше, чемAppendOne. Выделить требуемый объем памяти за один раз,sliceНет необходимости выделять память при расширении базового массива, а старые базовые данные можно использовать повторно, что, очевидно, снижает нагрузку на сборщик мусора.

Так же, новыйmapТакже можно указать размер.

func makeMap(num int){
    m := make(map[int]int,num)
    for i:=0;i<len(num);i++{
        m[i]=i
    }
}

Это может уменьшить накладные расходы на копирование памяти, а также уменьшить накладные расходы на повторное хеширование.

Храните в карте значения, а не указатели, используйте сегментированную карту

Посмотрите на следующий пример, карта сохраняет указатели и значения соответственно.

func timeGC() time.Duration {
	start := time.Now()
	runtime.GC()
	return time.Since(start)
}

func mapPointer(num int) {
	m := make(map[int]*int, num)
	for i := 0; i < num; i++ {
		m[i] = &i
	}
	runtime.GC()
	fmt.Printf("With %T, GC took %s\n", m, timeGC())
	_ = m[0]
}

func mapValue(num int) {
	m := make(map[int]int, num)
	for i := 0; i < num; i++ {
		m[i] = i
	}
	runtime.GC()
	fmt.Printf("With %T, GC took %s\n", m, timeGC())
	_ = m[0]
}

func mapPointerShard(num int) {
	shards := make([]map[int]*int, 100)
	for i := range shards {
		shards[i] = make(map[int]*int)
	}
	for i := 0; i < num; i++ {
		shards[i%100][i] = &i
	}
	runtime.GC()
	fmt.Printf("With map shards (%T), GC took %s\n", shards, timeGC())
	_ = shards[0][0]
}

func mapValueShard(num int) {
	shards := make([]map[int]int, 100)
	for i := range shards {
		shards[i] = make(map[int]int)
	}
	for i := 0; i < num; i++ {
		shards[i%100][i] = i
	}
	runtime.GC()
	fmt.Printf("With map shards (%T), GC took %s\n", shards, timeGC())
	_ = shards[0][0]
}

const N = 5e7 // 5000w

func BenchmarkMapPointer(b *testing.B) {
	mapPointer(N)
}

func BenchmarkMapValue(b *testing.B) {
	mapValue(N)
}

func BenchmarkMapPointerShard(b *testing.B) {
	mapPointerShard(N)
}

func BenchmarkMapValueShard(b *testing.B) {
	mapValueShard(N)
}

бегать

$ go test -bench=^BenchmarkMapPointer$ -benchmem
With map[int]*int, GC took 545.139836ms
goos: darwin
goarch: amd64
pkg: com.learn/gormLearn/gc_gc
BenchmarkMapPointer-4                  1        9532798100 ns/op        1387850488 B/op   724960 allocs/op

$ go test -bench=^BenchmarkMapPointerShard$ -benchmem
With map shards ([]map[int]*int), GC took 688.39764ms
goos: darwin
goarch: amd64
pkg: com.learn/gormLearn/gc_gc
BenchmarkMapPointerShard-4             1        20670458639 ns/op       4286763416 B/op  1901279 allocs/op

$ go test -bench=^BenchmarkMapValueShard$ -benchmem
With map shards ([]map[int]int), GC took 1.965519ms
goos: darwin
goarch: amd64
pkg: com.learn/gormLearn/gc_gc
BenchmarkMapValueShard-4               1        16190847776 ns/op       4385268936 B/op  1918445 allocs/op

$ go test -bench=^BenchmarkMapValue$ -benchmem 
With map[int]int, GC took 22.993926ms
goos: darwin
goarch: amd64
pkg: com.learn/gormLearn/gc_gc
BenchmarkMapValue-4            1        8253025035 ns/op        1444338752 B/op   724512 allocs/op

Как видите, использование сегментированной экономичной карты занимает меньше всего времени на сборку мусора. плюсGODEBUG=gctrace=1Проанализируйте трассировки GC:

$ GODEBUG=gctrace=1 go test -bench=^BenchmarkMapPointer$ -benchmem
...
gc 3 @0.130s 19%: 0.006+424+0.013 ms clock, 0.027+0.18/424/848+0.055 ms cpu, 1224->1224->1224 MB, 1225 MB goal, 4 P
gc 4 @9.410s 2%: 0.005+543+0.002 ms clock, 0.022+0/543/1628+0.011 ms cpu, 1325->1325->1323 MB, 2448 MB goal, 4 P (forced)
gc 5 @9.957s 3%: 0.003+547+0.003 ms clock, 0.013+0/547/1631+0.013 ms cpu, 1323->1323->1323 MB, 2647 MB goal, 4 P (forced)
With map[int]*int, GC took 550.40821ms

Чтобы понять печатный журнал, нам нужно понятьgctrace, от0.013+0/547/1631+0.013 ms cpuНапример, GC делится на три фазы.

  • Mark Prepare (STW).0.013Указывает глобальную остановку мирового времени для фазы маркировки.
  • Marking.0/547/1631, 0 означаетmutator assistкропотливый,547,1631На пометку GC уходит много времени.
  • Mark Termination (STW).0.013Указывает глобальную остановку мирового времени для обозначения конечной фазы.
$ GODEBUG=gctrace=1 go test -bench=^BenchmarkMapValue$ -benchmem
...
gc 3 @0.018s 0%: 0.005+0.14+0.015 ms clock, 0.021+0.054/0.020/0.19+0.060 ms cpu, 1224->1224->1224 MB, 1225 MB goal, 4 P
gc 4 @8.334s 0%: 0.006+21+0.003 ms clock, 0.027+0/6.4/21+0.013 ms cpu, 1379->1379->1334 MB, 2448 MB goal, 4 P (forced)
gc 5 @8.358s 0%: 0.003+19+0.003 ms clock, 0.014+0/5.0/20+0.015 ms cpu, 1334->1334->1334 MB, 2668 MB goal, 4 P (forced)

можно увидеть,mapСохранение значений занимает меньше времени, чем сохранение указателей, в основном на этапе разметки GC.

Преобразование строки и []byte

В Голанге,stringНеизменный по дизайну. следовательно,stringа также[]byteПреобразование типа заключается в создании новой копии.

func Example() {
	s := "Hello,world"
	b := []byte(s)
}

Если решили преобразоватьstring/[]byteОна не изменяется и может быть преобразована напрямую, так что копия исходной переменной не создается. Новая переменная разделяет базовый указатель данных.

func String2Bytes(s string) []byte {
	stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
	bh := reflect.SliceHeader{
		Data: stringHeader.Data,
		Len:  stringHeader.Len,
		Cap:  stringHeader.Len,
	}
	return *(*[]byte)(unsafe.Pointer(&bh))
}

func Bytes2String(b []byte) string {
	sliceHeader
	sh := reflect.StringHeader{
		Data: sliceHeader.Data,
		Len:  sliceHeader.Len,
	}
	return *(*string)(unsafe.Pointer(&sh))
}

Возвращаемые значения функции используют значения, а не указатели

Для функций, которые занимают меньше места и часто выделяются, если функция возвращает указатель, это вызовет утечку памяти, так что память, которая может быть выделена в стеке, должна быть выделена в куче. Копии небольших объектов в стеке работают хорошо, намного лучше, чем размещение объектов в куче. Посмотрите на следующий пример, две функции возвращают значение и указатель соответственно.

type S struct {
	a, b, c int64
	d, e, f string
	g, h, i float64
}

func byCopy() S {
	return S{
		a: 1, b: 1, c: 1,
		e: "lyp", f: "lyp",
		g: 1.0, h: 1.0, i: 1.0,
	}
}

func byPointer() *S {
	return &S{
		a: 1, b: 1, c: 1,
		e: "lyp", f: "lyp",
		g: 1.0, h: 1.0, i: 1.0,
	}
}

эталонная функция

func BenchmarkMemoryStack(b *testing.B) {
	var s S

	f, err := os.Create("stack.out")
	if err != nil {
		panic(err)
	}
	defer f.Close()

	err = trace.Start(f)
	if err != nil {
		panic(err)
	}

	for i := 0; i < b.N; i++ {
		s = byCopy()
	}

	trace.Stop()

	b.StopTimer()
	_ = fmt.Sprintf("%v", s.a)
}

func BenchmarkMemoryHeap(b *testing.B) {
	var s *S

	f, err := os.Create("heap.out")
	if err != nil {
		panic(err)
	}
	defer f.Close()

	err = trace.Start(f)
	if err != nil {
		panic(err)
	}

	for i := 0; i < b.N; i++ {
		s = byPointer()
	}

	trace.Stop()

	b.StopTimer()
	_ = fmt.Sprintf("%v", s.a)
}

бегать

 go test ./... -bench=BenchmarkMemoryHeap -benchmem -run=^$ -count=10           
goos: darwin
goarch: amd64
pkg: com.learn/gormLearn/gc_gc
BenchmarkMemoryHeap-4           19625536                53.0 ns/op            96 B/op          1 allocs/op

go test ./... -bench=BenchmarkMemoryStack -benchmem -run=^$ -count=10                                   
goos: darwin
goarch: amd64
pkg: com.learn/gormLearn/gc_gc
BenchmarkMemoryStack-4          163253341                7.22 ns/op            0 B/op          0 allocs/op

Как видите, время, затрачиваемое на выделение стека (возвращаемое значение), равно7.22 ns/op, а время выделения кучи (возврат указателя) составляет53.0 ns/op.

Используйте struct{} для оптимизации

В Голанге нет набора. Если вы хотите реализовать коллекцию, вы можете использоватьstruct{}как ценность.

func assign(num int) {
	m := make(map[int]bool, num)
	for i := 0; i < num; i++ {
		m[i] = true
	}
}

func assignStruct(num int) {
	m := make(map[int]struct{}, num)
	for i := 0; i < num; i++ {
		m[i] = struct{}{}
	}
}

struct{}После специальной оптимизации компилятором он указывает на тот же адрес памяти (runtime.zerobase) и не занимает места.

Инструменты для анализа ГХ

  • go tool pprof
  • go tool trace
  • иди строй -gcflags="-m"
  • GODEBUG="gctrace=1"

Мой публичный номер: где делится lyp

Колонка моих знаний

мой блог1

мой блог 2