04.04.2020 срез исходники аллокации
Как работать со срезами, объяснено в в блоге GO. Далее рассмотрим внутренее устройство среза.
Slice, слайс, срез — тип данных, построенный как обертка над массивом.
От массива срез отличает следующее:
В исходниках GO срез представлен следующей структурой:
type slice struct { array unsafe.Pointer len int cap int }
len (length, длина) — текущая длина среза, cap (capacity, вместимость) — длина внутреннего массива.
Оба эти параметра можно задать при вызове функции make
:
s := make( []int, 10, // len 10, // cap )
Cap — ключевой параметр для аллокации памяти, влияет на производительность вставки в срез.
Рассмотрим поведение среза при увеличении его размера.
a := []int{1} b := a[0:1] b[0] = 0 fmt.Println(a) // [0] a[0] = 1 fmt.Println(b) // [1]
Мы получили срез b из среза a. Далее видим, что изменение в одном срезе влияют на другой. Оба среза ссылаются на один и тот же массив.
Теперь дополним пример вставкой элементов в срез a:
a := []int{1} b := a[0:1] a = append(a, 2) a[0] = 10 fmt.Println(a, b) // [10 2] [0]
Теперь значения у элементов срезов разные. Теперь срезы ссылаются на разный массив.
Понять, почему так произошло, помогут исходники функции growslice.
В результате изменения cap среза всегда произойдет копирование данных массива:
memmove(p, old.array, lenmem)
Теперь рассмотрим, каким образом происходит фактическое увеличение cap:
newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { if old.len < 1024 { newcap = doublecap } else { // Check 0 < newcap to detect overflow // and prevent an infinite loop. for 0 < newcap && newcap < cap { newcap += newcap / 4 } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap <= 0 { newcap = cap } } }
При текущем размере среза менее 1024 элементов, размер памяти увеличивается вдвое (вне зависимости от запрашиваемой cap).
При размере среза > 1024 элементов, срез увеличивается на четверть текущего размера.
Приходим к выводу, что операция вставки в срез имеет серьезные последствия для памяти:
Делаем вывод — чтобы избежать новых аллокаций и копирования, нужно еще при создании среза выставлять большой cap.
В примере ниже мы изменили лишь cap у среза a. Но теперь после вставки в него, копирование не произошло и оба среза даже после вставки ссылаются на один массив:
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.
Но мы сможем лишь еще больше увеличить рост аллоцируемой памяти.
func AppendByte(slice []byte, data ...byte) []byte { m := len(slice) n := m + len(data) if n > cap(slice) { // if necessary, reallocate // allocate double what's needed, for future growth. newSlice := make([]byte, (n+1)*2) copy(newSlice, slice) slice = newSlice } slice = slice[0:n] copy(slice[m:n], data) return slice }
Стоит также быть внимательным при получении новых срезов из срезов большего размера. Сборщик мусора не удаляет весь массив, на который ссылается какой-либо срез, даже если срез ссылается всего на 1 элемент.