Здравствуйте, уважаемые, Гоферы ?
Позвольте мне начать с вопроса: Как бы вы протестировали производительность части кода или функции в Go? Если вы уже опытный разработчик здесь вы ничего нового не узнаете, но для новичков это отличная возможность узнать что-то новое и попрактиковаться.
В этом уроке я покажу вам, очень подробно, как использовать потрясающий инструмент бенчмаркинга, который встроен в пакет тестирования Golang.
Давайте начнём
Что такое Benchmark - тесты
В Go Benchmark - тесты используются для измерения производительности (скорости и использования памяти) функций или блоков кода. Эти тесты являются частью пакета тестирования Go и пишутся в тех же файлах, что и модульные тесты, но они предназначены специально для анализа производительности.
Пример использования:
Последовательность Фибоначчи
В этом примере я буду использовать классическую последовательность Фибоначчи, которая определяется:
if (x < 2)
F(0) = 1
F(2) = 2
else
F(x) = F(x-1) + F(x-2)
На практике последовательность такова:
1, 1, 2, 3, 5, 8, 13, etc.
Эта последовательность важна, потому что она встречается в различных разделах математики и природы, как показано ниже:
Существует несколько способов реализации этого кода, и я выберу два из них для нашего Benchmark - теста: рекурсивный и итерационный методы. Основная задача функций - задать позицию и вернуть число Фибоначчи в этой позиции.
Рекурсивный метод
// main.go
func fibRecursive(n uint) uint {
if n <= 2 {
return 1
}
return fibRecursive(n-1) + fibRecursive(n-2)
}
Приведенная выше функция - это рекурсивная реализация вычисления последовательности Фибоначчи. Давайте , для новичков, пошагово разберём её.
Вот наша функция для вычисления чисел Фибоначчи:
func fibRecursive(n uint) uint {
if n <= 2 {
return 1
}
return fibRecursive(n-1) + fibRecursive(n-2)
}
1. Функция
func fibRecursive(n uint) uint
func
: Это ключевое слово определяет функцию в Go.fibRecursive
: Это имя функции. Она называется fibRecursive, потому что вычисляет числа Фибоначчи с помощью рекурсии.n uint:
Функция принимает единственный аргумент, n, который имеет тип uint (целое число без знака). Он представляет собой позицию последовательности Фибоначчи, которую мы хотим вычислить.uint
: Функция возвращает uint (беззнаковое целое число), поскольку числа Фибоначчи - неотрицательные целые числа.
2. Базовый случай
if n <= 2 {
return 1
}
Оператор if проверяет, меньше или равно ли n чем 2 .
В последовательности Фибоначчи и 1-е, и 2-е числа равны 1. Поэтому, если n равно 1 или 2, функция возвращает 1.
Это называется базовым случаем, и он останавливает рекурсию от бесконечного углубления.
3. Рекурсивный случай
return fibRecursive(n-1) + fibRecursive(n-2)
Если n больше 2, функция вызывает себя дважды:
fibRecursive(n-1): Вычисляет число Фибоначчи для позиции, расположенной непосредственно перед n.
fibRecursive(n-2): Вычисляет число Фибоначчи для двух позиций перед n.
Затем функция складывает эти два результата вместе, поскольку каждое число Фибоначчи является суммой двух предыдущих чисел.
Итерационный метод
// main.go
func fibIterative(position uint) uint {
slc := make([]uint, position)
slc[0] = 1
slc[1] = 1
if position <= 2 {
return 1
}
var result, i uint
for i = 2; i < position; i++ {
result = slc[i-1] + slc[i-2]
slc[i] = result
}
return result
}
Этот код реализует итерационный подход к вычислению последовательности Фибоначчи в Go, который отличается от рекурсивного подхода. Также посмотрим как это работает:
1. Функция:
func fibIterative(position uint) uint
func:
Это ключевое слово объявляет функцию в Go.fibIterative
: Из названия функции следует, что она вычисляет числа Фибоначчи с помощью итерации (цикла).position uint
: Функция принимает один аргумент,position
, который является целым числом без знака(uint)
. Оно представляет собой позицию последовательности Фибоначчи, которую вы хотите вычислить.uint
: Функция возвращает целое число без знака (uint
), которое будет числом Фибоначчи в указанной позиции.
2. Создание слайса:
slc := make([]uint, position)
slc
- это слайс (динамический массив в Go), который создается с длинойposition
. В этом срезе будут храниться числа Фибоначчи по каждому индексу.
3. Начальные значения для последовательности Фибоначчи:
slc[0] = 1
slc[1] = 1
Первые два числа Фибоначчи оба равны 1
, поэтому первые две позиции в срезе (slc[0]
и slc[1]
) устанавливаются в 1
.
4. Минимальные значения последовательности:
if position <= 2 {
return 1
}
Если входная позиция равна 1 или 2, функция возвращает 1, поскольку первые два числа Фибоначчи всегда равны 1.
5. Итерационный цикл:
var result, i uint
for i = 2; i < position; i++ {
result = slc[i-1] + slc[i-2]
slc[i] = result
}
Цикл начинается с
i = 2
и выполняется до тех пор, пока не достигнет позиции.На каждой итерации число Фибоначчи с индексом i вычисляется как сумма двух предыдущих чисел Фибоначчи
(slc[i-1]
иslc[i-2])
.Результат сохраняется как в result, так и в срезе
slc[i]
для последующих вычислений.
6. Возвращение результата :
return result
После завершения цикла переменная
result
содержит число Фибоначчи в нужной позиции, и функция возвращает его.
Это более эффективный подход к вычислению чисел Фибоначчи по сравнению с рекурсией, особенно когда position
большое число, поскольку он не повторяет лишних вычислений, что мы и доказываем с помощью эталонных тестов. Давайте докажем это.
Как запустить Benchmark тесты?
Теперь, прежде чем запустить тесты, их нужно написать. Для начала вам нужно создать файл main_test.go. В нем, используя документацию Golang по benchmark тестам, вы можете создать тестируемые функции следующим образом:
// main_test.go
// Benchmark for Iterative Function
func BenchmarkFibIterative(b *testing.B) {
for i := 0; i < b.N; i++ {
fibIterative(uint(10))
}
}
// Benchmark for Recursive Function
func BenchmarkFibRecursive(b *testing.B) {
for i := 0; i < b.N; i++ {
fibRecursive(uint(10))
}
}
Давайте запустим тест для позиции 10, а затем увеличим ее соответствующим образом. Чтобы запустить тесты бенчмарков, достаточно выполнить команду
go test -bench=NameoftheFunction
Если вы хотите узнать больше об этой команде, загляните сюда. Давайте проверим функцию для позиции 10:
func BenchmarkFibIterative(b *testing.B) {
for i := 0; i < b.N; i++ {
fibIterative(uint(10))
}
}
Проанализируем это с помощью данного изображения:
Судя по изображению, у нас есть 8 ядер для тестов и нет ограничения по времени (они будут работать до завершения). Для выполнения задачи потребовалось 51_809_116 итераций и 2,158 секунды.
func BenchmarkFibRecursive(b *testing.B) {
for i := 0; i < b.N; i++ {
fibRecursive(uint(10))
}
}
Используя то же изображение для анализа результата, в данном случае для выполнения поставленной задачи потребовалось 9_827_694 итераций и 1,396 секунды:Результат для итеративной функции:
Функция Фибоначчи |
Позиция последовательности |
Кол-во итерация |
Время выполнения |
Итеративная |
10 |
51_809_116 |
2,158 |
Рекурсивная |
10 |
9_827_694 |
1,396 |
Результаты тестирования показывают, что итерационный подход значительно эффективнее рекурсивного для вычисления последовательности Фибоначчи.
Для позиции 10 итеративная функция выполнила примерно 51,8 миллиона итераций за 2,158 секунды, в то время как рекурсивная функция справилась лишь с 9,8 миллиона итераций за 1,396 секунды. Итеративный метод превзошел рекурсивный как по количеству итераций, так и по времени, что подчеркивает его эффективность.
Чтобы еще больше доказать это, давайте попробуем с позицией 40 (в 4 раза больше предыдущего значения):
Результат для итеративной функции:
Результат для рекурсивной функции:
Функция Фибоначчи |
Позиция последовательности |
Кол-во итерация |
Время выполнения |
Итеративная |
40 |
11_016_957 |
108,8 |
Рекурсивная |
40 |
5 |
209_418_983 |
Результаты бенчмарков наглядно демонстрируют разницу в эффективности итеративного и рекурсивного подходов для повторного вычисления Фибоначчи.
Итеративная функция выполнила около 11 миллиона итераций со средним временем выполнения 108,8 наносекунды на операцию, завершив тест за 2,477 секунды. Напротив, рекурсивная функция выполнила всего 5 итерации со средним временем выполнения 209 418 983 наносекунд на операцию , что потребовало 2,316 секунды для завершения работы.
Эти результаты показывают, что рекурсивный подход гораздо менее эффективен из-за повторных вызовов функций и пересчетов, поэтому итерационный метод значительно превосходит по скорости и использованию ресурсов, особенно при увеличении размера входных данных.
Просто из любопытства я попробовал позицию 60, и это буквально обрушило тест:
Итерационная функция выдала такие результаты:
А вот что выведет рекурсионная я ждал 2 минуты и так и не дождался, не став мучать свой ноутбук.
Заключение
Если ваш код работает медленно или непредсказуемо медленно, вы можете использовать эту технику в сочетании с pprof
или другими инструментами из встроенного пакета тестирования, чтобы определить и проверить, где ваш код работает плохо, и поработать над тем, как его оптимизировать.
Буду рад обратной связи. Всего доброго!