Глубокое погружение в бенчмаркинг на Голанге
Я не собираюсь лгать - сравнительный анализ не одна из моих самых сильных сторон. Я делаю это не так часто, как хотелось бы. Но это стало более частым с тех пор, как я начал использовать 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 раза быстрее, чем интерфейс.
На сегодня все, надеюсь, вы кое-что узнали о сравнительном анализе. Я точно знаю. Полный код можно найти здесь.
Не забудьте выйти на улицу и оценить мир.