Принципы работы типа slice в GO

4 апреля 2020 г. Slice Allocation Sources


Принципы работы типа slice в GO

В блоге Go описывается, как использовать срезы. Давайте посмотрим на внутреннее устройство срезов.

Срез — это тип данных, который оборачивает массив.

Различия между срезом и массивом:

  • Срез — это указатель на массив
  • Размер среза можно изменять

В исходниках Go срез представлен следующей структурой:

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

len (длина) — текущая длина среза, cap (ёмкость) — количество элементов в базовом массиве.

Оба поля можно передать как параметры функции make:

s := make(
	[]int,
	10, // len
	10, // cap
)

Ёмкость — основной параметр, отвечающий за выделение памяти в срезах. Она также отвечает за производительность append.

Поведение среза при добавлении элементов

Давайте посмотрим на поведение среза при добавлении элементов:

a := []int{1}
b := a[0:1]
b[0] = 0

fmt.Println(a)
// [0]

a[0] = 1

fmt.Println(b)
// [1]

Мы создали срез b из среза a. Как видим, оба среза указывают на один и тот же базовый массив.

Добавим операцию append к одному из срезов:

a := []int{1}
b := a[0:1]

a = append(a, 2)
a[0] = 10

fmt.Println(a, b)
// [10 2] [0]

После append срезы имеют разные значения в первом элементе. Теперь срезы указывают на разные массивы.

Эту ситуацию можно понять, изучив исходники функции growslice.

При изменении ёмкости данные базового массива всегда копируются:

memmove(p, old.array, lenmem)

Теперь давайте посмотрим на реальные изменения ёмкости:

newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
	newcap = cap
} else {
	if old.len < 1024 {
		newcap = doublecap
	} else {
		// Проверяем 0 < newcap для обнаружения переполнения
		// и предотвращения бесконечного цикла.
		for 0 < newcap && newcap < cap {
			newcap += newcap / 4
		}
		// Устанавливаем newcap в запрошенную cap, когда
		// вычисление newcap привело к переполнению.
		if newcap <= 0 {
			newcap = cap
		}
	}
}

Когда длина среза < 1024, размер памяти удваивается.

В противном случае срез растёт на четверть.

Влияние добавления элементов на память

Добавление элементов в срез оказывает значительное влияние на память:

  • При изменении ёмкости массив будет скопирован
  • Выделенная память будет расти согласно внутренней логике Go
  • Чтобы избежать выделений памяти, необходимо установить начальную ёмкость среза достаточно большой

В следующем примере единственное изменение — это ёмкость среза. Теперь после добавления элементов в срез изменения ёмкости не происходит. Оба среза по-прежнему указывают на один и тот же массив:

a := make([]int, 1, 2)
a[0] = 1

b := a[0:1]
b[0] = 0

fmt.Println(a, b)
// [0] [0]

a = append(a, 2)
a[0] = 10

fmt.Println(a, b)
// [10 2] [10]

В блоге Go описывается возможность использования другой функции append. Однако единственное, что мы можем сделать — это делать ещё более агрессивные выделения памяти, чем Go:

func AppendByte(slice []byte, data ...byte) []byte {
	m := len(slice)
	n := m + len(data)
	if n > cap(slice) { // при необходимости перевыделяем
		// выделяем вдвое больше необходимого для будущего роста.
		newSlice := make([]byte, (n+1)*2)
		copy(newSlice, slice)
		slice = newSlice
	}
	slice = slice[0:n]
	copy(slice[m:n], data)
	return slice
}

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

Tags:

Похожие статьи

23 Apr 2020

Профилирование Go: основы и практика

Профилирование Go: основы и практика

Go имеет богатые инструменты профилирования с самого начала — пакет pprof и go tool pprof. Давайте обсудим, почему профилирование полезно, как с ним работать и что нового в этой области.

Read More → Profiling Pprof Benchmark Ab Cpu Allocation Remote
9 Apr 2020

Шаблоны GO: принципы и использование

Шаблоны GO: принципы и использование

Пакеты text/template и html/template являются частью стандартной библиотеки Go. Шаблоны Go используются во многих программах, написанных на Go — Docker, Kubernetes, Helm. Многие сторонние библиотеки интегрированы с шаблонами Go, например Echo. Знание синтаксиса шаблонов Go очень полезно.

Эта статья состоит из документации пакета text/template и нескольких решений автора. После описания синтаксиса шаблонов Go мы погрузимся в исходники text/template и html/template.

Read More → Templates Html Text Sources
2 Apr 2020

Обработка данных в конкурентных программах

Обработка данных в конкурентных программах

В Go у нас есть функциональность горутин из коробки. Мы можем запускать код параллельно. Однако в нашем параллельно выполняющемся коде мы можем работать с общими переменными, и не совсем понятно, как именно Go обрабатывает такие ситуации.

Read More → Map Sources
2 Apr 2020

Принципы работы типа map в GO

Принципы работы типа map в GO

Программный интерфейс map в Go описан в блоге Go. Нам просто нужно вспомнить, что map — это хранилище ключ-значение, и оно должно извлекать значения по ключу как можно быстрее.

Read More → Map Sources
30 Mar 2020

Golang regexp: matching newline

Why PHP- and JavaScript-like regular expressions work with dot (".") work differently in GO.

Read More → Regular Expressions Sources
30 Mar 2020

Golang regexp: сопоставление символа новой строки

Golang regexp: сопоставление символа новой строки

Почему регулярные выражения с точкой (".") работают по-другому в Go по сравнению с PHP и JavaScript.

Read More → Regular Expressions Sources