Модуль Benchmark в Ruby
Каждому разработчику важно уметь оптимизировать свой код.
В языке Ruby в стандартной библиотеке уже существует необходимый инстуремент для измерения производительности кода - модуль Benchmark
(документация).
Ниже можно увидеть пример его использования из документации:
require 'benchmark'
puts Benchmark.measure { 'a' * 1_000_000_000 }
# => 0.365794 0.271074 0.636868 ( 0.644437)
Чтобы лучше понять как использовать данный инструмент, далее мы будем сравнивать 2 способа изменения массива:
- используя метод
map
def increase_using_map(nums_arr) nums_arr.map { |val| val + 5 } end
- и метод
each
.def increase_using_each(nums_arr) result = [] nums_arr.each { |val| result << (val + 5) } result end
Benchmark.measure
Самый простой способ измерить производительность кода - вызов Benchmark.measure
. Как уже было показано ранее, в метод необходимо передать блок кода, который будет измеряться. Чтобы увидеть результат метода, необходимо вызвать команду puts
. Давайте посмотрим примеры использования и результаты измерений на наших функциях:
require 'benchmark'
range = (1..10_000_000)
puts Benchmark.measure { increase_using_map(range) }
puts Benchmark.measure { increase_using_each(range) }
# 0.386068 0.010744 0.396812 ( 0.396839)
# 0.303948 0.007739 0.311687 ( 0.311698)
# => nil
Этих данных достаточно, чтобы понять, что метод increase_using_each
исполнился быстрее, чем increase_using_map
. Однако есть метод более удобный для сравнения, который мы рассмотрим далее.
Benchmark.bm
Benchmark.bm
- более продвинутый метод для сравнения производительности блоков кода, который позволяет видеть результаты в одном месте. В отличие от простого использования Benchmark.measure
, здесь каждый блок кода оборачивается в bm.report
— нужно указать название отчета и передать сам блок, который мы измеряем. Ниже представлен пример его использования:
require 'benchmark'
range = (1..10_000_000)
Benchmark.bm do |bm|
bm.report('.map') do
increase_using_map(range)
end
bm.report('.each') do
increase_using_each(range)
end
end
# user system total real
# .map 0.379351 0.009624 0.388975 ( 0.388993)
# .each 0.306710 0.008468 0.315178 ( 0.315204)
# =>
# [#<Benchmark::Tms:0x00000001008baed8
# @cstime=0.0,
# @cutime=0.0,
# @label=".map",
# @real=0.38899300002958626,
# @stime=0.009623999999999994,
# @total=0.38897500000000007,
# @utime=0.37935100000000005>,
# #<Benchmark::Tms:0x00000001008b8188
# @cstime=0.0,
# @cutime=0.0,
# @label=".each",
# @real=0.3152039999840781,
# @stime=0.00846799999999999,
# @total=0.315178,
# @utime=0.30671000000000004>]
Как видите, для каждого блока кода вызывается bm.report
, где первым аргументом передаётся описание замера, что добавляет наглядности. При этом нет необходимости использовать puts
для вывода, так как Benchmark.bm
автоматически форматирует результаты в виде таблицы.
Кроме того, этот метод возвращает массив объектов класса Benchmark::Tms
— специальных объектов (data object), которые представляют собой детализированные результаты измерения и позволяют легко работать с ними, как с объектами. Подробности можно найти в документации Benchmark::Tms.
Benchmark.bmbm
При измерении производительности код иногда сталкивается с накладными расходами на сбор мусора, что может искажать результаты. Чтобы минимизировать эти помехи, существует метод Benchmark.bmbm
.
require 'benchmark'
range = (1..10_000_000)
Benchmark.bmbm do |bm|
bm.report('.map') do
increase_using_map(range)
end
bm.report('.each') do
increase_using_each(range)
end
end
# Rehearsal -----------------------------------------
# .map 0.382209 0.009926 0.392135 ( 0.392142)
# .each 0.304083 0.009298 0.313381 ( 0.313406)
# -------------------------------- total: 0.705516sec
#
# user system total real
# .map 0.341568 0.007595 0.349163 ( 0.349168)
# .each 0.306042 0.009010 0.315052 ( 0.315067)
# =>
# [#<Benchmark::Tms:0x0000000100e8ec88
# @cstime=0.0,
# @cutime=0.0,
# @label=".map",
# @real=0.34916800004430115,
# @stime=0.007594999999999796,
# @total=0.34916299999999945,
# @utime=0.34156799999999965>,
# #<Benchmark::Tms:0x0000000100e8ed78
# @cstime=0.0,
# @cutime=0.0,
# @label=".each",
# @real=0.3150669999886304,
# @stime=0.009009999999999962,
# @total=0.31505200000000055,
# @utime=0.3060420000000006>]
Benchmark.bmbm
сначала проводит репетицию (Rehearsal), выполняя каждый блок кода, чтобы стабилизировать среду выполнения и уменьшить влияние «холодного» запуска. Затем запускается реальное измерение. Перед ним вызывается GC.start
, чтобы сборщик мусора Ruby выполнил свою работу заранее и не вмешивался в результаты измерений.
Данный метод позволяет более точно измерить время выполнения кода, однако не гарантирует 100% изоляции от сторонних эфеектов.
Как интерпретировать результаты?
Теперь мы умеем измерять производительность кода, но давайте разберёмся, что означает каждый столбец в результатах:
- user (
@utime
вBenchmark::Tms
) - время, потраченное на выполнение кода самим процессором (CPU) в режиме пользователя, включающее операции, выполняемые на уровне приложения, но исключает время, затраченное операционной системой на обработку системных вызовов. - system (
@stime
вBenchmark::Tms
) - время, потраченное на выполнение системных вызовов операционной системы для выполнения блока кода. Это может включать операции чтения/записи данных, обращения к файловой системе, сеть и другие действия, требующие помощи ОС. - total (
@total
вBenchmark::Tms
) - cумма user и system, представляющая собой общее процессорное время, затраченное на выполнение блока кода. Но этот показатель не всегда совпадает с real, поскольку real может включать периоды ожидания, если код был временно приостановлен. - real (
@real
вBenchmark::Tms
) - время выполнения кода “в реальности”, то есть суммарное время от начала до завершения выполнения блока кодаб, который также включает задержки на уровне операционной системы и ожидание, если код был приостановлен.
В каких случая стоит использовать модуль Benchmark?
- Сравнение алгоритмов сортировки
- Оптимизация запросов к БД
- Оптимизация фоновых задач
- Оптимизация производительности веб-запросов
- Оптимизация работы с коллекциями данных (как в наших примерах) и др.
Заключение
Модуль Benchmark
в Ruby — это мощный и удобный инструмент, позволяющий разработчикам анализировать и оптимизировать производительность кода. Важно понимать, что измерение времени выполнения — это не просто способ сделать код «быстрее». Они помогают находить узкие места и принимать обоснованные решения о том, какие части приложения требуют внимания для достижения высокой производительности и улучшения пользовательского опыта.