Есть некоторые проблемы с реализацией Erlang. В качестве базового показателя для следующего, мое измеренное время выполнения вашей немодифицированной программы Erlang составило 47,6 секунды, по сравнению с 12,7 секундами для кода C.
Первое, что вы должны сделать, если хотите запустить вычислительный код Erlang, - это использовать нативный код. Компиляция с erlc +native euler12
сократила время до 41,3 секунды. Это, однако, намного более низкое ускорение (всего 15%), чем ожидалось от нативной компиляции для такого рода кода, и проблема заключается в использовании -compile(export_all)
. Это полезно для экспериментов, но тот факт, что все функции потенциально доступны извне, приводит к тому, что нативный компилятор очень консервативен. (Обычный эмулятор BEAM не так сильно затронут.) Замена этого объявления на -export([solve/0]).
дает гораздо лучшее ускорение: 31,5 секунды (почти 35% от базовой линии).
Но сам код имеет проблему: для каждой итерации в цикле factorCount вы выполняете этот тест:
factorCount (_, Sqrt, Candidate, Count) when Candidate == Sqrt -> Count + 1;
Код C не делает этого. В общем, может быть сложно провести справедливое сравнение между различными реализациями одного и того же кода, и в частности, если алгоритм является числовым, потому что вы должны быть уверены, что они на самом деле делают одно и то же. Небольшая ошибка округления в одной реализации из-за некоторого типа преобразования где-то может привести к тому, что он выполнит намного больше итераций, чем другая, даже если обе в конечном итоге достигнут одного и того же результата.
Чтобы устранить этот возможный источник ошибок (и избавиться от дополнительного теста в каждой итерации), я переписал функцию factorCount следующим образом, тесно по образцу кода C:
factorCount (N) ->
Sqrt = math:sqrt (N),
ISqrt = trunc(Sqrt),
if ISqrt == Sqrt -> factorCount (N, ISqrt, 1, -1);
true -> factorCount (N, ISqrt, 1, 0)
end.
factorCount (_N, ISqrt, Candidate, Count) when Candidate > ISqrt -> Count;
factorCount ( N, ISqrt, Candidate, Count) ->
case N rem Candidate of
0 -> factorCount (N, ISqrt, Candidate + 1, Count + 2);
_ -> factorCount (N, ISqrt, Candidate + 1, Count)
end.
Это переписывание, без export_all
и нативная компиляция, дало мне следующее время выполнения:
$ erlc +native euler12.erl
$ time erl -noshell -s euler12 solve
842161320
real 0m19.468s
user 0m19.450s
sys 0m0.010s
, что не так уж плохо по сравнению с кодом C:
$ time ./a.out
842161320
real 0m12.755s
user 0m12.730s
sys 0m0.020s
Учитывая, что Эрланг вовсе не ориентирован на написание числового кода, он всего на 50% медленнее, чем С в такой программе, как это, довольно хорошо.
Наконец, по поводу ваших вопросов:
Вопрос 1: Erlang, python и haskell теряют скорость из-за использования целых чисел произвольной длины или
не так ли они, пока значения меньше MAXINT?
Да, в некоторой степени. В Erlang нет никакого способа сказать «использовать 32/64-битную арифметику с циклическим изменением», поэтому, если компилятор не может доказать какие-либо ограничения на ваши целые числа (а это обычно не может), он должен проверить все вычисления, чтобы увидеть могут ли они вписаться в одно помеченное слово или если оно должно превратить их в бигнумы, выделенные для кучи. Даже если на практике во время выполнения не используются никакие бигнумы, эти проверки должны быть выполнены. С другой стороны, это означает, что вы знаете , что алгоритм никогда не потерпит неудачу из-за неожиданного целочисленного переноса, если вы вдруг дадите ему больше входных данных, чем раньше.
Вопрос 4. Разрешают ли мои функциональные реализации LCO и, следовательно, избегают добавления ненужных кадров в стек вызовов?
Да, ваш код Erlang верен в отношении оптимизации последнего вызова.