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 против 4123 нс / оп

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

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

Заметки

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

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

3 ответа

Решение

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

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

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

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

LEAQ    (DX)(SI*1),BX

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

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],

на go1.18 и M1 разница намного больше, я был уверен, что массив быстрее, чем слайс, но теперь у меня есть доказательство, что это не всегда так.

      goos: darwin
goarch: arm64
BenchmarkSliceGlobal-8        926196          1257.0 ns/op         0 B/op          0 allocs/op
BenchmarkArrayGlobal-8       2110324           567.0 ns/op         0 B/op          0 allocs/op
BenchmarkSliceLocal-8        2275382           535.0 ns/op         0 B/op          0 allocs/op
BenchmarkArrayLocal-8        1802491           647.4 ns/op         0 B/op          0 allocs/op```
Другие вопросы по тегам