Подтвердить что ты не робот

Почему Enum.concat намного медленнее, чем ++ при конкатенации списков?

Я попытался выполнить быстрый бенчмаркинг с помощью Benchfella:

 
defmodule ConcatListBench do
  use Benchfella

  @a1 Enum.to_list(1..10_000)
  @a2 Enum.to_list(10_000..20_0000)

  bench "++" do
    @a1 ++ @a2
  end

  bench "Enum.concat" do
    Enum.concat(@a1, @a2)
  end
end

И при запуске:

$ elixir -v
Erlang/OTP 19 [erts-8.0.2] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Elixir 1.4.0-dev (762e7de)

$ mix bench
Settings:
  duration:      1.0 s

## ConcatListBench
[10:01:09] 1/2: ++
[10:01:20] 2/2: Enum.concat

Finished in 14.03 seconds

## ConcatListBench
benchmark na iterations   average time
++           1000000000   0.01 µs/op
Enum.concat       50000   45.03 µs/op

Вопрос в том, как Enum.concat может быть медленнее (более 4k раз), если он использует ++ оператор внутри для списков?

Я понимаю, что предложения охраны в Enum.concat и сопоставлении шаблонов некоторое время, но эта таблица показывает большую разницу, не так ли?

ОБНОВЛЕНИЕ: Это происходит из-за Constant Folding, объединение с использованием ++ оптимизировано во время компиляции и мгновенно принимает время для запуска. Таким образом, эталон не совсем реалистичен.

4b9b3361

Ответ 1

Короткий ответ: Постоянная сгибание.

Более длинный ответ: атрибуты модуля в Elixir заменяются литеральными значениями, когда Elixir скомпилирован в файлы beam. Например, следующий код:

defmodule ConcatListBench do
  @a1 Enum.to_list(1..10)
  @a2 Enum.to_list(10..20)

  def plusplus, do: @a1 ++ @a2

  def concat, do: Enum.concat(@a1, @a2)
end

скомпилируется:

-module('Elixir.ConcatListBench').
... 
concat() ->
    'Elixir.Enum':concat([1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
             [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]).

plusplus() ->
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ++
      [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20].

Модуль компилятора Erlang sys_core_fold, который выполняет постоянную оптимизацию сгибания, оценивает операции ++ как можно больше во время компиляции. Поскольку в этом случае оба списка являются литералами, он может полностью исключить вызов функции и заменить его на полученный список. Поэтому в вашем тесте функция ++ просто возвращает список, который уже существует в виртуальной машине. Это выполняется так же быстро, как и выполнение 1 + 2 (которое также постоянно сгибается до 3):

...
bench "1 + 2" do
  1 + 2
end
...
## ConcatListBench
benchmark na iterations   average time
1 + 2        1000000000   0.01 µs/op
++           1000000000   0.01 µs/op
Enum.concat       50000   37.89 µs/op

Более реалистичным эталоном было бы сделать косвенный вызов ++, который компилятор Erlang не сбрасывает:

def plus_plus(a, b), do: a ++ b

bench "++" do
  plus_plus(@a1, @a2)
end

Это выходы из 3 прогонов:

## ConcatListBench
benchmark na iterations   average time
Enum.concat       50000   37.44 µs/op
++                50000   41.65 µs/op

## ConcatListBench
benchmark na iterations   average time
++                50000   36.07 µs/op
Enum.concat       50000   38.58 µs/op

## ConcatListBench
benchmark na iterations   average time
Enum.concat       50000   39.34 µs/op
++                50000   40.74 µs/op

Итак, если ваши списки не являются постоянными во время компиляции, оба способа так же быстры. Я ожидал бы, что Enum.concat будет немного медленнее (особенно для небольших списков), так как он немного работает больше, чем ++.