Fork me on GitHub

Benchee

Это перевод актуальной версии оригинального урока.

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

Содержание

О Benchee

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

Использование

Чтобы использовать Benchee в своём проекте, просто добавим её в качестве зависимости в mix.exs:

defp deps do
  [{:benchee, "~> 0.9", only: :dev}]
end

Теперь вызовем:

$ mix deps.get
...
$ mix compile

Первая команда загрузит и установит Benchee. Возможно, понадобится также установить Hex. Вторая команда скомпилирует Benchee. Всё готово к написанию нашего первого теста!

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

list = Enum.to_list(1..10_000)
map_fun = fn i -> [i, i * i] end

Benchee.run(%{
  "flat_map" => fn -> Enum.flat_map(list, map_fun) end,
  "map.flatten" => fn -> list |> Enum.map(map_fun) |> List.flatten() end
})

Теперь, чтобы произвести замер производительности, исполним код в файле:

$ mix run benchmark.exs

В консоли должно появиться что-то похожее:

Operating System: macOS
CPU Information: Intel(R) Core(TM) i5-4260U CPU @ 1.40GHz
Number of Available Cores: 4
Available memory: 8.589934592 GB
Elixir 1.5.1
Erlang 20.0
Benchmark suite executing with the following configuration:
warmup: 2.00 s
time: 5.00 s
parallel: 1
inputs: none specified
Estimated total run time: 14.00 s


Benchmarking flat_map...
Benchmarking map.flatten...

Name                  ips        average  deviation         median
flat_map           1.03 K        0.97 ms    ±33.00%        0.85 ms
map.flatten        0.56 K        1.80 ms    ±31.26%        1.60 ms

Comparison:
flat_map           1.03 K
map.flatten        0.56 K - 1.85x slower

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

Взглянув на секцию Comparison, сразу становится понятно, что реализация map.flatten в 1,85 раз медленнее, чем flat_map. Очень полезно! Но давайте взглянем и на другие метрики:

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

Настройка

Одна из лучших сторон Benchee — это большое количество настроек. Мы пройдемся сперва по простейшим из них, т.к. они понятны даже без примеров кода. Затем мы посмотрим, как использовать одну из самых крутых возможностей Benchee — наборы входных данных.

Основы

Benchee использует для настройки параметры конфигурации. В основном интерфейсе Benchee.run/2 они передаются вторым аргументом в виде опционального ключевого списка.

Benchee.run(
  %{"example function" => fn -> "hi!" end},
  warmup: 4,
  time: 10,
  inputs: nil,
  parallel: 1,
  formatters: [&Benchee.Formatters.Console.output/1],
  print: [
    benchmarking: true,
    configuration: true,
    fast_warning: true
  ],
  console: [
    comparison: true,
    unit_scaling: :best
  ]
)

Список доступных параметров (также описан в документации):

Наборы входных данных

Очень важно тестировать функции с использованием данных, похожих на те, с которыми они скорее всего столкнутся в реальном мире. Часто функция может вести себя по-разному на маленьких и больших наборах данных. Специально для таких случаев Benchee поддерживает наборы входных данных (inputs)! Они позволяют нам тестировать одну и ту же функцию со столькими наборами входных данных, сколько нам понадобится, а затем отобразить их в результатах.

Посмотрим на наш первый пример:

list = Enum.to_list(1..10_000)
map_fun = fn i -> [i, i * i] end

Benchee.run(%{
  "flat_map" => fn -> Enum.flat_map(list, map_fun) end,
  "map.flatten" => fn -> list |> Enum.map(map_fun) |> List.flatten() end
})

В нём мы используем один-единственный список целых чисел от 1 до 10 000. Давайте вместо него используем несколько разных аргументов, чтобы можно было сравнить результаты на малых и больших списках. Должно получиться так:

map_fun = fn i -> [i, i * i] end

inputs = %{
  "small list" => Enum.to_list(1..100),
  "medium list" => Enum.to_list(1..10_000),
  "large list" => Enum.to_list(1..1_000_000)
}

Benchee.run(
  %{
    "flat_map" => fn list -> Enum.flat_map(list, map_fun) end,
    "map.flatten" => fn list -> list |> Enum.map(map_fun) |> List.flatten() end
  },
  inputs: inputs
)

Можно заметить два существенных различия. Во-первых, мы создали ассоциативный массив inputs, в котором хранится информация о наборах входных данных. Во-вторых, мы передаём его в виде настройки в Benchee.run/2.

И так как теперь наша функция должна уметь принимать входной аргумент, мы должны заменить:

fn -> Enum.flat_map(list, map_fun) end

на:

fn(list) -> Enum.flat_map(list, map_fun) end

Снова запустим наши измерения:

$ mix run benchmark.exs

Теперь должно получиться что-то такое:

Operating System: macOS
CPU Information: Intel(R) Core(TM) i5-4260U CPU @ 1.40GHz
Number of Available Cores: 4
Available memory: 8.589934592 GB
Elixir 1.5.1
Erlang 20.0
Benchmark suite executing with the following configuration:
warmup: 2.00 s
time: 5.00 s
parallel: 1
inputs: large list, medium list, small list
Estimated total run time: 2.10 min

Benchmarking with input large list:
Benchmarking flat_map...
Benchmarking map.flatten...

Benchmarking with input medium list:
Benchmarking flat_map...
Benchmarking map.flatten...

Benchmarking with input small list:
Benchmarking flat_map...
Benchmarking map.flatten...


##### With input large list #####
Name                  ips        average  deviation         median
flat_map             6.29      158.93 ms    ±19.87%      160.19 ms
map.flatten          4.80      208.20 ms    ±23.89%      200.11 ms

Comparison:
flat_map             6.29
map.flatten          4.80 - 1.31x slower

##### With input medium list #####
Name                  ips        average  deviation         median
flat_map           1.34 K        0.75 ms    ±28.14%        0.65 ms
map.flatten        0.87 K        1.15 ms    ±57.91%        1.04 ms

Comparison:
flat_map           1.34 K
map.flatten        0.87 K - 1.55x slower

##### With input small list #####
Name                  ips        average  deviation         median
flat_map         122.71 K        8.15 μs   ±378.78%        7.00 μs
map.flatten       86.39 K       11.58 μs   ±680.56%       10.00 μs

Comparison:
flat_map         122.71 K
map.flatten       86.39 K - 1.42x slower

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

Форматтеры

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

Другие форматтеры

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

Каждый из этих форматтеров поставляется отдельным пакетом, поэтому, для использования, их нужно добавить в качестве зависимостей в mix.exs:

defp deps do
  [
    {:benchee_csv,  "~> 0.6", only: :dev},
    {:benchee_json, "~> 0.3", only: :dev},
    {:benchee_html, "~> 0.3", only: :dev},
  ]
end

В то время как benchee_json и benchee_csv очень просты, benchee_html можно назвать крайне продвинутым! Он может с легкостью строить красивые графики и диаграммы, и даже позволяет экспортировать их в изображение формата PNG. Для всех трёх форматтеров написана хорошая документация на их страницах на GitHub, поэтому мы не будем вдаваться в детали.

Собственные форматтеры

Если ни один из имеющихся форматтеров не подходит, всегда можно написать собственный. Сделать это несложно. Надо написать функцию, которая будет принимать структуру %Benchee.Suite{}, из которой она в дальнейшем будет доставать любую нужную информацию. Подробнее о том, что именно находится в структуре, можно почитать на GitHub или в HexDocs. Код очень хорошо документирован и легко читается.

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

defmodule Custom.Formatter do
  def output(suite) do
    suite
    |> format
    |> IO.write()

    suite
  end

  defp format(suite) do
    Enum.map_join(suite.scenarios, "\n", fn scenario ->
      "Average for #{scenario.job_name}: #{scenario.run_time_statistics.average}"
    end)
  end
end

И теперь запустим измерения:

list = Enum.to_list(1..10_000)
map_fun = fn(i) -> [i, i * i] end

Benchee.run(%{
  "flat_map"    => fn -> Enum.flat_map(list, map_fun) end,
  "map.flatten" => fn -> list |> Enum.map(map_fun) |> List.flatten end
}, formatters: [&Custom.Formatter.output/1])

Наш форматтер должен вывести следующее:

Operating System: macOS
CPU Information: Intel(R) Core(TM) i5-4260U CPU @ 1.40GHz
Number of Available Cores: 4
Available memory: 8.589934592 GB
Elixir 1.5.1
Erlang 20.0
Benchmark suite executing with the following configuration:
warmup: 2.00 s
time: 5.00 s
parallel: 1
inputs: none specified
Estimated total run time: 14.00 s


Benchmarking flat_map...
Benchmarking map.flatten...
Average for flat_map: 851.8840109326956
Average for map.flatten: 1659.3854339873628

Contributors

loading...



Поделиться