Array vs Slice: доступ к скорости

Этот вопрос касается скорости доступа к элементам массивов и срезов, а не к эффективности передачи их функциям в качестве аргументов.

Я бы ожидал, что массивы будут быстрее, чем ломтики, в большинстве случаев, потому что срез представляет собой структуру данных, описывающую непрерывный раздел массива, и поэтому может быть дополнительная шаг при доступе к элементам среза (косвенно - элементам его базового массива).

Итак, я написал небольшой тест, чтобы сравнить партию простых операций. Существует 4 контрольных функции, первые 2 - тест глобальный срез и глобальный массив, остальные 2 - срез локальный и локальный массив:

var gs = make([]byte, 1000) // Global slice
var ga [1000]byte           // Global array

func BenchmarkSliceGlobal(b *testing.B) {
    for i := 0; i < b.N; i++ {
        for j, v := range gs {
            gs[j]++; gs[j] = gs[j] + v + 10; gs[j] += v
        }
    }
}

func BenchmarkArrayGlobal(b *testing.B) {
    for i := 0; i < b.N; i++ {
        for j, v := range ga {
            ga[j]++; ga[j] = ga[j] + v + 10; ga[j] += v
        }
    }
}

func BenchmarkSliceLocal(b *testing.B) {
    var s = make([]byte, 1000)
    for i := 0; i < b.N; i++ {
        for j, v := range s {
            s[j]++; s[j] = s[j] + v + 10; s[j] += v
        }
    }
}

func BenchmarkArrayLocal(b *testing.B) {
    var a [1000]byte
    for i := 0; i < b.N; i++ {
        for j, v := range a {
            a[j]++; a[j] = a[j] + v + 10; a[j] += v
        }
    }
}

Я провел тест несколько раз, вот типичный вывод (go test -bench .*):

BenchmarkSliceGlobal      300000              4210 ns/op
BenchmarkArrayGlobal      300000              4123 ns/op
BenchmarkSliceLocal       500000              3090 ns/op
BenchmarkArrayLocal       500000              3768 ns/op

Анализ результатов:

Доступ к глобальному срезу немного медленнее, чем доступ к глобальному массиву, который, как я ожидал:
4210 vs 4123 ns/op

Но доступ к локальному срезу значительно быстрее, чем доступ к локальному массиву:
3090 vs 3768 ns/op

Мой вопрос: В чем причина этого?

Примечания

Я пробовал различать следующие вещи, но никто не изменил результат:

  • размер массива/среза (проверено 100, 1000, 10000)
  • порядок эталонных функций
  • тип элемента массива/среза (проверены byte и int)

Ответы

Ответ 1

Сравнивая сборку amd64 как BenchmarkArrayLocal, так и BenchmarkSliceLocal (слишком долго, чтобы вписаться в этот пост):

Версия массива загружает адрес a из памяти несколько раз, практически на каждую операцию доступа к массиву:

LEAQ    "".a+1000(SP),BX

В то время как версия среза вычисляется исключительно на регистрах после загрузки один раз из памяти:

LEAQ    (DX)(SI*1),BX

Это не является убедительным, но, вероятно, причиной. Причина в том, что оба метода в действительности практически идентичны. Еще одна примечательная деталь заключается в том, что версия массива вызывает runtime.duffcopy, что является довольно длительной процедурой сборки, тогда как в версии среза нет.

Ответ 2

Go версии 1.8 может устранить некоторые проверки диапазона, чтобы разница стала больше.

BenchmarkSliceGlobal-4 500000 3220 ns/op BenchmarkArrayGlobal-4 1000000 1287 ns/op BenchmarkSliceLocal-4 1000000 1267 ns/op BenchmarkArrayLocal-4 1000000 1301 ns/op

Для массивов я бы рекомендовал использовать размеры от двух степеней и включать логические операции и операции. Таким образом, вы уверены, что компилятор исключает проверку. Таким образом, var ga [1024]byte с ga[j & 1023].