Какой смысл устанавливать емкость среза?

В Golang мы можем использовать встроенную функцию make() для создания среза с заданной начальной длиной и емкостью.

Рассмотрим следующие строки: длина среза равна 1, а его емкость 3:

func main() {
    var slice = make([]int, 1, 3)
    slice[0] = 1
    slice = append(slice, 6, 0, 2, 4, 3, 1)
    fmt.Println(slice)
}

Я был удивлен, увидев, что эта программа печатает:

[1 6 0 2 4 3 1]

Это заставило меня задаться вопросом: в чем смысл изначально определять емкость среза, если append() может просто пропустить его? Есть ли прирост производительности для установки достаточно большой емкости?

Ответ 1

Ломтик - это на самом деле просто модный способ управления базовым массивом. Он автоматически отслеживает размер и перераспределяет новое пространство по мере необходимости.

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

Когда вы make предоставляете емкость среза, вы устанавливаете начальную емкость, а не какой-либо предел.

Посмотрите этот пост в блоге о срезах, чтобы узнать некоторые интересные внутренние детали срезов.

Ответ 2

slice - замечательная абстракция простого array. Вы получаете все виды приятных функций, но в основе лежит array. (Я объясняю следующее в обратном порядке по причине). Поэтому, если/когда вы задаете capacity из 3, в глубине памяти в памяти выделяется массив длины 3, который вы можете использовать до append без необходимости перераспределения памяти. Этот атрибут является необязательным в команде make, но обратите внимание, что у slice всегда будет capacity, независимо от того, выберете ли вы его. Если вы укажете length (который также всегда существует), slice будет индексируемым до этой длины. Остальная часть capacity скрыта за кулисами, поэтому ей не нужно выделять совершенно новый массив при использовании append.

Вот пример, чтобы лучше объяснить механику.

s := make([]int, 1, 3)

Базовому array будет присвоен 3 нулевого значения int (то есть 0):

[0,0,0]

Однако length установлен на 1, поэтому сам срез будет печатать только [0], и если вы попытаетесь проиндексировать второе или третье значение, он будет panic, поскольку механика slice этого не делает. разрешить это. Если вы s = append(s, 1) к нему, вы обнаружите, что он действительно был создан, чтобы содержать значения zero вплоть до length, и вы получите [0,1]. В этот момент вы можете append еще раз, прежде чем весь базовый array будет заполнен, а другой append заставит его выделить новое и скопировать все значения с удвоенной емкостью. На самом деле это довольно дорогая операция.

Поэтому краткий ответ на ваш вопрос заключается в том, что предварительное выделение capacity может быть использовано для значительного повышения эффективности вашего кода. Особенно, если slice либо будет очень большим, либо содержит сложный structs (или оба), поскольку значение zero для struct фактически является значением zero для каждого из его fields. Это не потому, что это позволило бы избежать выделения этих значений, как в любом случае, а потому, что append пришлось бы перераспределять новый array, полный этих нулевых значений, каждый раз, когда ему нужно было бы изменять размер базового массива.

Короткий пример игровой площадки: https://play.golang.org/p/LGAYVlw-jr