Семь мифов о производительности Erlang

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

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

Миф 1: Хвостовая рекурсия гораздо быстрее, чем обычные рекурсивные функции

Согласно этому мифу, вариант функции для построения списка, который сначала при помощи хвостовой рекурсии строит его в обратном порядке, а затем разворачивает при помощи lists:reverse/1, будет работать быстрее, чем вариант, которые при помощи обычной рекурсии сразу строит правильно ориентированный список. И происходит это по причине того, что при обычной рекурсии производит больше выделений памяти, чем при хвостовой.

В некоторой степени это было верно до релиза R12B. Еще правдивее это утверждение было до релиза R7B. Но сегодня это не так актуально. Сегодня обычная рекурсия обычно использует столько же памяти, что и хвостовая. Как правило, невозможно предсказать, какой вариант будет быстрее. Поэтому используйте тот вариант, который сделает ваш код более читабельным (чаще всего это обычная рекурсия).

Более подробный разбор этого вопроса можно прочитать в статье "Хвостовая рекурсия в Erlang - это не серебряная пуля (англ)".

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

Миф 2: Оператор ++ это всегда плохо

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

naive_reverse([H|T]) ->
    naive_reverse(T)++[H];
naive_reverse([]) ->
    [].

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

Но вот такой вариант использования ++ совсем не плох:

naive_but_ok_reverse([H|T], Acc) ->
    naive_but_ok_reverse(T, [H]++Acc);
naive_but_ok_reverse([], Acc) ->
    Acc.

Здесь каждый элемент списка копируется только один раз. Аккумулируемый результат Acc является правым операндом и не копируется.

Хотя опытные Erlang разработчики напишут эту функцию следующим образом:

vanilla_reverse([H|T], Acc) ->
    vanilla_reverse(T, [H|Acc]);
vanilla_reverse([], Acc) ->
    Acc.

Этот вариант чуть более эффективен т.к. здесь не создается список из одного элемента только для того, чтобы скопировать этот элемент.

Миф 3: Строки это медленно

Обработка строк действительно может быть медленной, если выполнять ее неправильно. В Erlang к работе со строками надо подходить ответственно и выбирать правильные инструменты. Например, для работы с регулярными выражениями стоит использовать модуль re из STDLIB вместо устаревшего модуля regexp.

Миф 4: Исправление Dets файлов это очень медленно

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

Миф 5: BEAM это стековая виртуальная машина для байт-кода и поэтому она медленная

BEAM - это регистровая виртуальная машина. Она имеет 1024 виртуальных регистра, которые используются для хранения временных значений и передачи аргументов в функции. Переменные, которые должны пережить вызов функции, сохраняются в стек.

BEAM - это виртуальная машина, использующая шитый код(threaded code). Каждая инструкция указывает непосредственно на исполняемый C-код, что делает вызов инструкций очень быстрым.

Миф 6: Для ускорения программы нужно использовать _, когда переменная не используется

Раньше это действительно было так. Но после релиза R6B компилятор BEAM умеет сам находить неиспользуемые переменные.

Это касается и других тривиальных оптимизаций на уровне исходного кода программы. Например замены case ... of оператора на охранные выражения. Теперь такие преобразования производятся компилятором автоматически и не влияют на результат.

Миф 7: Использование NIF всегда ускоряет ваши программы

Вынос логики из Erlang части в NIF, ради его ускорения, нужно рассматривать, как самое последнее средство. Это гарантирует, что ваш код станет небезопасным, но не гарантирует того, что он станет быстрее.

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

Это вольный перевод статьи "The Seven Myths of Erlang Performance".

Коментарии

Используйте Markdown

Thank you for comment!
Ваше сообщение будет доступно после проверки.