Глубокое погружение в бенчмаркинг на Голанге

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

Go позволяет разработчикам тестировать тесты с помощью пакета тестирования. Таким образом, пакет тестирования включает в себя возможности тестирования. Это потрясающе!

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

Давайте поговорим о сравнительном анализе. Бенчмаркинг в разработке программного обеспечения - это проверка производительности написанного нами кода.

Тест - это запуск« компьютерной программы , набора программ или других операций для оценки относительной производительности объекта». - Википедия

Бенчмаркинг позволяет нам выбирать разные решения и проверять их производительность, сравнивая измеренные скорости. Это отличные знания, которыми должен обладать разработчик, особенно когда у вас есть приложение, которое нужно ускорить и оптимизировать.

При разработке важно помнить золотое правило: никогда не оптимизируйте преждевременно. Тот факт, что мы научимся проводить тесты, не означает, что я предлагаю запускать и тестировать каждый фрагмент кода, который у вас есть. Я твердо уверен, что тестирование - это инструмент, который можно использовать, когда вы сталкиваетесь с проблемами производительности или когда вас убивает чистое любопытство.

«Преждевременная оптимизация - корень всех зол».

- Дональд Э. Кнут, «Искусство компьютерного программирования»

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

Давайте придерживаться выражения наиболее производительный, поскольку иногда более медленный код легче поддерживать и читать. Таким образом, этот код лучше, если вы спросите меня, если, конечно, вы не сталкиваетесь с проблемами производительности.

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

Мы для него посмотрим на них.

  • Срезы или карты быстрее?
  • Влияет ли размер на скорость срезов и карт?
  • Имеет ли значение тип ключа, используемый на картах?

Написание сверхпростого теста

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

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

go mod init benching

Вам также необходимо создать файл, заканчивающийся на _test.go. В моем случае это benching_test.go.

Тесты в Go выполняются с помощью пакета тестирования, как и обычные модульные тесты. Как и модульные тесты, тесты запускаются с помощью того же инструментария Go Test.

Инструмент Go узнает, какие методы являются эталонными, на основе их названий. Любой метод, начинающийся с Benchmark, который принимает указатель на testing.B, будет работать в качестве теста производительности.

package benching
import (
"testing"
)
func BenchmarkSimplest(b *testing.B) {
}

Попробуйте это, запустив команду go test с флагом -bench=..

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

Название говорит само за себя. Это имя мы установили в тестовом файле.

Интересно количество запусков теста. Каждый тест выполняется несколько раз, и каждое выполнение рассчитывается по времени. Затем время выполнения усредняется в зависимости от количества запусков. Это хорошо, поскольку однократный запуск теста обеспечит плохую статистическую корректность.

ns/op означает наносекунды на операцию. Это время, которое потребовалось для вызова метода.

Если у вас есть несколько тестов и вы хотите запустить только один или несколько, вы можете заменить точку на строку, совпадающую с именами типа -bench=BenchmarkSimplest. Помните, что выражение -bench=Benchmark по-прежнему будет запускать наш тест, поскольку строка соответствует началу метода.

Итак, прямо сейчас мы можем измерить скорость, но это не всегда все, что мы хотим измерить. К счастью, если мы заглянем в пакет тестирования, мы обнаружим, что добавление флага -benchmem добавит информацию о байтах, выделенных на операцию (B / op) и выделенных на операцию (allocs / op).

Если вы не знакомы с распределением памяти и памятью, я могу порекомендовать статью Винсента Бланшона, найденную здесь.

Скоро мы будем готовы приступить к сравнительному анализу реальных вещей - просто потерпите еще несколько минут. Что случилось с входным параметром в нашем тесте *testing.B? Давайте посмотрим на определение этого слова в стандартной библиотеке, чтобы узнать, с чем мы имеем дело.

// B is a type passed to Benchmark functions to manage benchmark
// timing and to specify the number of iterations to run.
//
// A benchmark ends when its Benchmark function returns or calls any of the methods
// FailNow, Fatal, Fatalf, SkipNow, Skip, or Skipf. Those methods must be called
// only from the goroutine running the Benchmark function.
// The other reporting methods, such as the variations of Log and Error,
// may be called simultaneously from multiple goroutines.
//
// Like in tests, benchmark logs are accumulated during execution
// and dumped to standard output when done. Unlike in tests, benchmark logs
// are always printed, so as not to hide output whose existence may be
// affecting benchmark results.
type B struct {
common
importPath string // import path of the package containing the benchmark
context *benchContext
N int
previousN int // number of iterations in the previous run
previousDuration time.Duration // total duration of the previous run
benchFunc func(b *B)
benchTime benchTimeFlag
bytes int64
missingBytes bool // one of the subbenchmarks does not have bytes set.
timerOn bool
showAllocResult bool
result BenchmarkResult
parallelism int // RunParallel creates parallelism*GOMAXPROCS goroutines
// The initial states of memStats.Mallocs and memStats.TotalAlloc.
startAllocs uint64
startBytes uint64
// The net total of this test after being run.
netAllocs uint64
netBytes uint64
// Extra metrics collected by ReportMetric.
extra map[string]float64
}

Testing.B - это структура, содержащая любые данные, относящиеся к работающему тесту. Он также содержит структуру с именем BenchmarkResult, которая используется для форматирования вывода. Если вы что-то не понимаете, я настоятельно рекомендую открыть benchmark.go и прочитать код.

Обратите внимание на одну важную вещь - переменную N. Помните, как тесты выполняются много раз? Сколько раз выполняются тесты, указывается переменной N внутри testing.B.

Согласно документации, это необходимо учитывать в тестах, поэтому давайте обновим BenchmarkSimplest, чтобы учесть N.

// BenchmarkSimplestNTimes runs a benchmark N amount of times.
func BenchmarkSimplestNTimes(b *testing.B) {
for i := 0; i < b.N; i++ {
// Run function to benchmark here
}
}

Мы обновили его, создав for цикл, который будет повторяться N раз. Когда я тестирую тесты, мне нравится устанавливать N на определенные значения, поэтому я удостоверяюсь, что мои тесты справедливы. В противном случае один тест может выполняться 100 000 раз, а другой - дважды.

Это можно сделать, добавив флаг -benchtime=. Вводится либо секунды, либо X раз, поэтому для принудительного выполнения тестов 100 раз мы можем установить значение -benchtime=100x.

Готово, готово, эталон!

Пришло время начать тестирование и ответить на поставленные ранее вопросы о производительности.

  • Срезы или карты быстрее?
  • Влияет ли размер на скорость срезов и карт?
  • Имеет ли значение используемый ключ?

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

// insertXIntMap is used to add X amount of items into a Map[int]int
func insertXIntMap(x int, b *testing.B) {
// Initialize Map and Insert X amount of items
testmap := make(map[int]int, 0)
// Reset timer after Initalizing map, that's not what we want to test
b.ResetTimer()
for i := 0; i < x; i++ {
// Insert value of I into I key.
testmap[i] = i
}
}

Этот метод принимает целочисленное значение того, сколько целых чисел нужно вставить в карту. Это сделано для того, чтобы мы могли проверить, влияет ли размер карты на производительность вставки. Этот метод будет выполняться нашими тестами. Я также создам несколько тестовых функций, каждая из которых вставляет разные целые числа для тестирования.

// BenchmarkInsertIntMap100000 benchmarks the speed of inserting 100000 integers into the map.
func BenchmarkInsertIntMap100000(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXIntMap(100000, b)
}
}
// BenchmarkInsertIntMap10000 benchmarks the speed of inserting 10000 integers into the map.
func BenchmarkInsertIntMap10000(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXIntMap(10000, b)
}
}
// BenchmarkInsertIntMap1000 benchmarks the speed of inserting 1000 integers into the map.
func BenchmarkInsertIntMap1000(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXIntMap(1000, b)
}
}
// BenchmarkInsertIntMap100 benchmarks the speed of inserting 100 integers into the map.
func BenchmarkInsertIntMap100(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXIntMap(100, b)
}
}

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

Таким образом, количество времени увеличивается - это вполне ожидаемо, поскольку мы увеличиваем количество вставок. Это пока мало что говорит нам, так как нам нужно с чем сравнивать результаты. Однако мы можем найти время, чтобы ответить на один вопрос: ли имеет значение тип ключа, используемый в картах?

Я собираюсь скопировать все методы и заменить используемый тип ключа на интерфейс. Чтобы упростить задачу, у меня теперь есть два файла: benching_map_interface_test.go и benching_map_int_test.go. Методы тестирования будут соотноситься с названием - это просто для того, чтобы поддерживать удобную для навигации структуру, когда мы добавляем дополнительные тесты.

package benching
import (
"testing"
)
// insertXInterfaceMap is used to add X amount of items into a Map[interface]int
func insertXInterfaceMap(x int, b *testing.B) {
// Initialize Map and Insert X amount of items
testmap := make(map[interface{}]int, 0)
// Reset timer after Initalizing map, that's not what we want to test
b.ResetTimer()
for i := 0; i < x; i++ {
// Insert value of I into I key.
testmap[i] = i
}
}
// BenchmarkInsertInterfaceMap1000000 benchmarks the speed of inserting 1000000 integers into the map.
func BenchmarkInsertInterfaceMap1000000(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXInterfaceMap(1000000, b)
}
}
// BenchmarkInsertInterfaceMap100000 benchmarks the speed of inserting 100000 integers into the map.
func BenchmarkInsertInterfaceMap100000(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXInterfaceMap(100000, b)
}
}
// BenchmarkInsertInterfaceMap10000 benchmarks the speed of inserting 10000 integers into the map.
func BenchmarkInsertInterfaceMap10000(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXInterfaceMap(10000, b)
}
}
// BenchmarkInsertInterfaceMap1000 benchmarks the speed of inserting 1000 integers into the map.
func BenchmarkInsertInterfaceMap1000(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXInterfaceMap(1000, b)
}
}
// BenchmarkInsertInterfaceMap100 benchmarks the speed of inserting 100 integers into the map.
func BenchmarkInsertInterfaceMap100(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXInterfaceMap(100, b)
}
}

Думаю, мы пока нашли ответ как минимум на один вопрос. Тип ключа, похоже, имеет значение, как мы можем видеть по результату. Использование Int вместо Interface в качестве ключа в этом тесте в 2,23 раза быстрее, с учетом теста 1000000. Однако я никогда раньше не видел, чтобы интерфейс использовался в качестве ключа.

Удвоение производительности на основе ключей, похоже, соответствует заключению, сделанному мной Джейсоном Мойроном. Он написал Go Performance Tales, что очень читается.

  • Срезы или карты быстрее?
  • Влияет ли размер на скорость срезов и карт?
  • D̸o̸e̸s̸ ̸t̸h̸e̸ ̸ke̸y̸ ̸t̸y̸p̸e̸ ̸u̸s̸e̸d̸ ̸i̸n̸ ̸m̸a̸p̸s̸ ̸m̸a̸t̸t̸e̸r̸? Да.

Прежде чем мы продолжим, я хотел бы воспользоваться моментом и добавить новый тест, так как это весело. В тестах, которые мы только что запустили, размер карт не был заранее выделен. Так что мы можем изменить это и измерить разницу.

Что нам нужно изменить, так это метод insertXIntMap, и мы также должны изменить инициализацию карты, чтобы вместо этого использовать длину X. Я создал один новый файл, benching_map_prealloc_int_test.go, и в нем изменил метод insertXIntMap, чтобы заранее инициализировать размер.

// insertXPreallocIntMap is used to add X amount of items into a Map[int]int
func insertXPreallocIntMap(x int, b *testing.B) {
// Initialize Map and Insert X amount of items and Prealloc the size to X
testmap := make(map[int]int, x)
// Reset timer after Initalizing map, that's not what we want to test
b.ResetTimer()
for i := 0; i < x; i++ {
// Insert value of I into I key.
testmap[i] = i
}
}

Помните, как я сказал, что мы можем контролировать, какие тесты производительности запускать, используя флаг -bench=? Пришло время использовать этот трюк, потому что теперь у нас есть много тестов. Но для этого конкретного теста меня интересует только сравнение карты без заданного размера и карты с заранее заданным размером.

Я назвал свои новые тесты BenchmarkInsertIntMapPrealloc, поэтому они имеют то же имя, что и BenchmarkInsertIntMap. Мы можем использовать это в качестве триггера. Этот новый файл эталонного теста является точной копией другого IntMap эталонного теста - я изменил только названия и метод выполнения.

package benching
import (
"testing"
)
// insertXPreallocIntMap is used to add X amount of items into a Map[int]int
func insertXPreallocIntMap(x int, b *testing.B) {
// Initialize Map and Insert X amount of items and Prealloc the size to X
testmap := make(map[int]int, x)
// Reset timer after Initalizing map, that's not what we want to test
b.ResetTimer()
for i := 0; i < x; i++ {
// Insert value of I into I key.
testmap[i] = i
}
}
// BenchmarkInsertIntMapPreAlloc1000000 benchmarks the speed of inserting 1000000 integers into the map.
func BenchmarkInsertIntMapPreAlloc1000000(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXPreallocIntMap(1000000, b)
}
}
// BenchmarkInsertIntMapPreAlloc100000 benchmarks the speed of inserting 100000 integers into the map.
func BenchmarkInsertIntMapPreAlloc100000(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXPreallocIntMap(100000, b)
}
}
// BenchmarkInsertIntMapPreAlloc10000 benchmarks the speed of inserting 10000 integers into the map.
func BenchmarkInsertIntMapPreAlloc10000(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXPreallocIntMap(10000, b)
}
}
// BenchmarkInsertIntMapPreAlloc1000 benchmarks the speed of inserting 1000 integers into the map.
func BenchmarkInsertIntMapPreAlloc1000(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXPreallocIntMap(1000, b)
}
}
// BenchmarkInsertIntMapPreAlloc100 benchmarks the speed of inserting 100 integers into the map.
func BenchmarkInsertIntMapPreAlloc100(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXPreallocIntMap(100, b)
}
}

Давайте запустим тест и изменим флаг теста.

go test -bench=BenchmarkInsertIntMap -benchmem -benchtime=100x

Этот тест показывает нам, что установка размера карты имеет довольно большое влияние. На самом деле это когда вы видите, что тест 1000000 имеет разницу в производительности в 1,92 раза. И посмотреть на выделенные байты (B / op) - намного приятнее.

Перейдем к реализации теста на вставку срезов. Это будет копия реализации карты, но на этот раз с использованием фрагмента с append.

Я также собираюсь создать тесты для предварительно выделенных срезов и тесты для нераспределенных размеров, так как это было очень весело. Мы можем просто воссоздать метод insertX, скопировать и вставить все, а затем найти Map и заменить его на Slice.

// insertXIntSlice is used to add X amount of items into a []int
func insertXIntSlice(x int, b *testing.B) {
// Initalize a Slice and insert X amount of Items
testSlice := make([]int, 0)
// reset timer
b.ResetTimer()
for i := 0; i < x; i++ {
testSlice = append(testSlice, i)
}
}
package benching
import (
"testing"
)
// insertXIntSlice is used to add X amount of items into a []int
func insertXIntSlice(x int, b *testing.B) {
// Initalize a Slice and insert X amount of Items
testSlice := make([]int, 0)
// reset timer
b.ResetTimer()
for i := 0; i < x; i++ {
testSlice = append(testSlice, i)
}
}
// BenchmarkInsertIntSlice1000000 benchmarks the speed of inserting 1000000 integers into the Slice.
func BenchmarkInsertIntSlice1000000(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXIntSlice(1000000, b)
}
}
// BenchmarkInsertIntSlice100000 benchmarks the speed of inserting 100000 integers into the Slice.
func BenchmarkInsertIntSlice100000(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXIntSlice(100000, b)
}
}
// BenchmarkInsertIntSlice10000 benchmarks the speed of inserting 10000 integers into the Slice.
func BenchmarkInsertIntSlice10000(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXIntSlice(10000, b)
}
}
// BenchmarkInsertIntSlice1000 benchmarks the speed of inserting 1000 integers into the Slice.
func BenchmarkInsertIntSlice1000(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXIntSlice(1000, b)
}
}
// BenchmarkInsertIntSlice100 benchmarks the speed of inserting 100 integers into the Slice.
func BenchmarkInsertIntSlice100(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXIntSlice(100, b)
}
}

Для предварительно выделенного фрагмента мы не хотим использовать append, поскольку это добавляет индекс к фрагменту. Таким образом, необходимо изменить предварительно выделенный индекс, чтобы вместо него использовался правильный индекс.

// insertXPreallocIntSlice is used to add X amount of items into a []int
func insertXPreallocIntSlice(x int, b *testing.B) {
// Initalize a Slice and insert X amount of Items
testSlice := make([]int, x)
// reset timer
b.ResetTimer()
for i := 0; i < x; i++ {
testSlice[i] = i
}
}
package benching
import (
"testing"
)
// insertXPreallocIntSlice is used to add X amount of items into a []int
func insertXPreallocIntSlice(x int, b *testing.B) {
// Initalize a Slice and insert X amount of Items
testSlice := make([]int, x)
// reset timer
b.ResetTimer()
for i := 0; i < x; i++ {
testSlice[i] = i
}
}
// BenchmarkInsertIntSlicePreAllooc1000000 benchmarks the speed of inserting 1000000 integers into the Slice.
func BenchmarkInsertIntSlicePreAllooc1000000(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXPreallocIntSlice(1000000, b)
}
}
// BenchmarkInsertIntSlicePreAllooc1000000 benchmarks the speed of inserting 100000 integers into the Slice.
func BenchmarkInsertIntSlicePreAllooc100000(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXPreallocIntSlice(100000, b)
}
}
// BenchmarkInsertIntSlicePreAllooc10000 benchmarks the speed of inserting 10000 integers into the Slice.
func BenchmarkInsertIntSlicePreAllooc10000(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXPreallocIntSlice(10000, b)
}
}
// BenchmarkInsertIntSlicePreAllooc1000 benchmarks the speed of inserting 1000 integers into the Slice.
func BenchmarkInsertIntSlicePreAllooc1000(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXPreallocIntSlice(1000, b)
}
}
// BenchmarkInsertIntSlicePreAllooc100 benchmarks the speed of inserting 100 integers into the Slice.
func BenchmarkInsertIntSlicePreAllooc100(b *testing.B) {
for i := 0; i < b.N; i++ {
insertXPreallocIntSlice(100, b)
}
}

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

Разница между предварительно выделенными фрагментами и динамическими фрагментами огромна. 1000000 Тесты 75388 ns/op против 7246 ns/op. Это разница в производительности в 10,4 раза выше скорости. Однако в некоторых случаях работа с фрагментами фиксированного размера может вызывать затруднения. Обычно я не знаю размера своих приложений, поскольку они обычно динамические.

Кажется, что срезы превосходят карты, когда дело доходит до вставки данных - как для небольших, так и для больших чисел. Нам также необходимо сравнить, как работает выбор данных.

Чтобы проверить это, мы инициализируем срез и карту, как мы это делали, добавляем X количество элементов, а затем сбрасываем таймер. Затем мы начнем сравнивать, насколько быстро мы можем найти X элементов. Я решил перебрать срез и сопоставить, используя значение индекса i. Я опубликую код обоих тестов ниже - они почти идентичны.

package benching
import (
"testing"
)
// selectXIntMap is used to Select X amount of times from a map
func selectXIntMap(x int, b *testing.B) {
// Initialize Map and Insert X amount of items
testmap := make(map[int]int, x)
// Reset timer after Initalizing map, that's not what we want to test
for i := 0; i < x; i++ {
// Insert value of I into I key.
testmap[i] = i
}
// holder is a holder that we use to hold the found int, we cannot grab from a map without storing the result
var holder int
b.ResetTimer()
for i := 0; i < x; i++ {
// Select from map
holder = testmap[i]
}
// Compiler wont let us get away with an unused holder, so a quick check will trick the compiler
if holder != 0 {
}
}
// BenchmarkSelectIntMap1000000 benchmarks the speed of selecting 1000000 items from the map.
func BenchmarkSelectIntMap1000000(b *testing.B) {
for i := 0; i < b.N; i++ {
selectXIntMap(1000000, b)
}
}
// BenchmarkSelectIntMap100000 benchmarks the speed of selecting 100000 items from the map.
func BenchmarkSelectIntMap100000(b *testing.B) {
for i := 0; i < b.N; i++ {
selectXIntMap(100000, b)
}
}
// BenchmarkSelectIntMap10000 benchmarks the speed of selecting 10000 items from the map.
func BenchmarkSelectIntMap10000(b *testing.B) {
for i := 0; i < b.N; i++ {
selectXIntMap(10000, b)
}
}
// BenchmarkSelectIntMap1000 benchmarks the speed of selecting 1000 items from the map.
func BenchmarkSelectIntMap1000(b *testing.B) {
for i := 0; i < b.N; i++ {
selectXIntMap(1000, b)
}
}
// BenchmarkSelectIntMap100 benchmarks the speed of selecting 100 items from the map.
func BenchmarkSelectIntMap100(b *testing.B) {
for i := 0; i < b.N; i++ {
selectXIntMap(100, b)
}
}
package benching
import (
"testing"
)
// selectXIntSlice is used to Select X amount of times from a Slice
func selectXIntSlice(x int, b *testing.B) {
// Initialize Slice and Insert X amount of items
testSlice := make([]int, x)
// Reset timer after Initalizing Slice, that's not what we want to test
for i := 0; i < x; i++ {
// Insert value of I into I key.
testSlice[i] = i
}
// holder is a holder that we use to hold the found int, we cannot grab from a Slice without storing the result
var holder int
b.ResetTimer()
for i := 0; i < x; i++ {
// Select from Slice
holder = testSlice[i]
}
// Compiler wont let us get away with an unused holder, so a quick check will trick the compiler
if holder != 0 {
}
}
// BenchmarkSelectIntSlice1000000 benchmarks the speed of selecting 1000000 items from the Slice.
func BenchmarkSelectIntSlice1000000(b *testing.B) {
for i := 0; i < b.N; i++ {
selectXIntSlice(1000000, b)
}
}
// BenchmarkSelectIntSlice100000 benchmarks the speed of selecting 100000 items from the Slice.
func BenchmarkSelectIntSlice100000(b *testing.B) {
for i := 0; i < b.N; i++ {
selectXIntSlice(100000, b)
}
}
// BenchmarkSelectIntSlice10000 benchmarks the speed of selecting 10000 items from the Slice.
func BenchmarkSelectIntSlice10000(b *testing.B) {
for i := 0; i < b.N; i++ {
selectXIntSlice(10000, b)
}
}
// BenchmarkSelectIntSlice1000 benchmarks the speed of selecting 1000 items from the Slice.
func BenchmarkSelectIntSlice1000(b *testing.B) {
for i := 0; i < b.N; i++ {
selectXIntSlice(1000, b)
}
}
// BenchmarkSelectIntSlice100 benchmarks the speed of selecting 100 items from the Slice.
func BenchmarkSelectIntSlice100(b *testing.B) {
for i := 0; i < b.N; i++ {
selectXIntSlice(100, b)
}
}

Если вы заметили, код для selectXIntSlice и selectXIntMap одинаковый - единственная разница - это команда make. Однако разница в производительности для этих двух устройств очевидна.

Сравнение результатов тестов

Итак, теперь у нас есть контрольные цифры - давайте сгруппируем их в таблицу, чтобы ее было легче просматривать.

Benchmark Type ns/op B/op allocs/op Fixed-size
Insert Map[interface]int 3730206 1318557 10380 no
Insert Map[int]int 1632196 879007 381 no
Insert Map[int]int 857588 1569 0 yes
Insert []int 75388 451883 0 no
Insert []int 7246 0 0 yes
Select Map[int]int 507843 0 0 yes
Select []int 2866 0 0 yes

Так какая разница между фрагментами и картами?

Срезы быстрее в 21,65 раза (1321196/75388) при сравнении производительности записи в динамический размер.

Срезы быстрее в 118,35 раза (857588/7246) при сравнении производительности записи в заранее выделенный размер.

Срезы быстрее в 177,19 раза (507843/2866) при сравнении скорости чтения.

Срезы или карты быстрее?

Срезы, похоже, намного превосходят карты при использовании этих тестов. Разница настолько велика, что я думаю, что, должно быть, как-то облажался с этими тестами.

Однако карты проще в использовании. В этих тестах мы предполагаем, что нам известны индексы в срезе, которые нужно использовать. Я могу вспомнить множество случаев, когда мы не знаем индекса и, вероятно, придется перебирать весь фрагмент, например, map[userID]User вместо цикла for над []User.

Влияет ли размер на скорость срезов и карт?

Размер в этих случаях не имеет значения.

Имеет ли значение тип ключа, используемый на картах?

Да. Использование целого числа оказалось в 2,23 раза быстрее, чем интерфейс.

Добавление более реалистичного варианта использования

Таким образом, срезы кажутся намного более производительными, но я буду честен - я вряд ли когда-либо узнаю правильный индекс для своих срезов. В большинстве случаев мне приходится перебирать весь фрагмент, чтобы найти то, что я ищу. Это основная причина, по которой я часто использую карту.

Я собираюсь создать тест с этим вариантом использования. У нас будут map[userID]User и []User. Тестом будет гонка по поиску определенного пользователя.

Я создал новый файл, содержащий код для генерации случайных пользователей. Я создам 10 000, 100 000 и 1 миллион пользователей в срезе и на карте. Представьте, что если бы у нас был API, нам отправили идентификатор пользователя, и мы хотели бы найти этого пользователя. Это сценарий, который мы и проверим. Я также буду перетасовать фрагмент, поскольку это имитирует реальный вариант использования, когда данные добавляются динамически.

Я называю этот тест «Спасение рядового Райана». Нам нужно его найти, а у него ID пользователя 7777.

package benching
import (
"math/rand"
"testing"
"time"
)
// Create slices and Maps to use in benchmark so we dont have to recreate them each itteration
var userSlice10000 []User
var userSlice100000 []User
var userSlice1000000 []User
var userMap10000 map[int]User
var userMap100000 map[int]User
var userMap1000000 map[int]User
// Names Some Non-Random name lists used to generate Random Users
var Names []string = []string{"Jasper", "Johan", "Edward", "Niel", "Percy", "Adam", "Grape", "Sam", "Redis", "Jennifer", "Jessica", "Angelica", "Amber", "Watch"}
// SirNames Some Non-Random name lists used to generate Random Users
var SirNames []string = []string{"Ericsson", "Redisson", "Edisson", "Tesla", "Bolmer", "Andersson", "Sword", "Fish", "Coder"}
var IDCounter int
type User struct {
ID int
Name string
}
func init() {
userMap10000 = generateMapUsers(10000)
userMap100000 = generateMapUsers(100000)
userMap1000000 = generateMapUsers(1000000)
userSlice10000 = generateSliceUsers(10000)
userSlice100000 = generateSliceUsers(100000)
userSlice1000000 = generateSliceUsers(1000000)
// Shuffle the slice
rand.Shuffle(len(userSlice10000), func(i, j int) {
userSlice10000[i], userSlice10000[j] = userSlice10000[j], userSlice10000[i]
})
rand.Shuffle(len(userSlice100000), func(i, j int) {
userSlice100000[i], userSlice100000[j] = userSlice100000[j], userSlice100000[i]
})
rand.Shuffle(len(userSlice1000000), func(i, j int) {
userSlice1000000[i], userSlice1000000[j] = userSlice1000000[j], userSlice1000000[i]
})
}
// generateSliceUsers will generate X amount of users in a slice
func generateSliceUsers(x int) []User {
IDCounter = 0
users := make([]User, x)
for i := 0; i <= x; i++ {
users = append(users, generateRandomUser())
}
return users
}
// generateSliceUsers will generate X amount of users in a slice
func generateMapUsers(x int) map[int]User {
IDCounter = 0
users := make(map[int]User, x)
for i := 0; i <= x; i++ {
users[i] = generateRandomUser()
}
return users
}
// generate a RandomUser
func generateRandomUser() User {
rand.Seed(time.Now().UnixNano())
nameMax := len(Names)
sirNameMax := len(SirNames)
nameIndex := rand.Intn(nameMax-1) + 1
sirNameIndex := rand.Intn(sirNameMax-1) + 1
id := IDCounter
IDCounter++
return User{
ID: id,
Name: Names[nameIndex] + " " + SirNames[sirNameIndex],
}
}
// findUserInSlice is used to search a slice of users for a userID
func findUserInSlice(userID int, users []User) {
// Now find the userID provided
for _, user := range users {
if user.ID == userID {
return
}
}
}
// findUserInMap is used to search a Map of users for a particular ID
func findUserInMap(userID int, users map[int]User) {
if _, ok := users[userID]; ok {
return
}
}
// BenchmarkFindUserInSlice1000000 will search in a slice of size 1000000 to find a user ID
func BenchmarkFindUserInSlice1000000(b *testing.B) {
for i := 0; i < b.N; i++ {
findUserInSlice(7777, userSlice1000000)
}
}
// BenchmarkFindUserInSlice100000 will search in a slice of size 100000 to find a user ID
func BenchmarkFindUserInSlice100000(b *testing.B) {
for i := 0; i < b.N; i++ {
findUserInSlice(7777, userSlice100000)
}
}
// BenchmarkFindUserInSlice10000 will search in a slice of size 10000 to find a user ID
func BenchmarkFindUserInSlice10000(b *testing.B) {
for i := 0; i < b.N; i++ {
findUserInSlice(7777, userSlice10000)
}
}
// BenchmarkFindUserInMap1000000 will search in a Map of size 1000000 to find a user ID
func BenchmarkFindUserInMap1000000(b *testing.B) {
for i := 0; i < b.N; i++ {
findUserInMap(7777, userMap1000000)
}
}
// BenchmarkFindUserInMap100000 will search in a Map of size 100000 to find a user ID
func BenchmarkFindUserInMap100000(b *testing.B) {
for i := 0; i < b.N; i++ {
findUserInMap(7777, userMap100000)
}
}
// BenchmarkFindUserInMap10000 will search in a Map of size 10000 to find a user ID
func BenchmarkFindUserInMap10000(b *testing.B) {
for i := 0; i < b.N; i++ {
findUserInMap(7777, userMap100000)
}
}

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

Type Users ns/op
Slice 10000 1763
Slice 100000 72760
Slice 100000 140917
Map 10000 21.8
Map 100000 19.5
Map 100000 21.1

В этом случае карта более производительна с коэффициентом 6678,53x (140917 / 21,1).

Заключение

Срезы или карты быстрее?

Срезы гораздо более производительны, когда дело доходит до чистой мощности, но менее сложны и сложнее в использовании - как это продемонстрировано с помощью нашего теста «Спасти рядового Райана». Иногда власть - это еще не все.

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

Влияет ли размер на скорость срезов и карт?

К сожалению для меня, моя жена говорит, что размер имеет значение. Мой тест говорит то же самое. При использовании правильного порядкового номера - конечно, не имеет значения. Но если вы не знаете, в каком индексе хранится ваше значение, размер имеет большое значение.

Имеет ли значение тип ключа, используемый на картах?

Да. Использование целого числа оказалось в 2,23 раза быстрее, чем интерфейс.

На сегодня все, надеюсь, вы кое-что узнали о сравнительном анализе. Я точно знаю. Полный код можно найти здесь.

Не забудьте выйти на улицу и оценить мир.