Я все еще на начальной стадии изучения OCaml, и мне любопытно узнать, каков наилучший способ извлечь максимальную производительность из общего кода в OCaml.
В качестве небольшого эксперимента я написал две полиморфные функции: одну в C ++ и другую в OCaml, которая находит самый большой элемент в данном массиве.
Я заметил, что в то время как в C ++ вы не платите штраф за подобную абстракцию, штраф в OCaml — это колоссальное падение производительности на один градус. Кроме того, решение C ++, которое я быстро придумал, является более общим, чем решение OCaml, но я виню в этом, в первую очередь, мою неопытность по отношению к языку.
Мой вопрос заключается в следующем: как писать и использовать полиморфные функции в OCaml, не платя при этом огромных потерь производительности, которые я только что наблюдал?
Еще одна вещь, которую я заметил для этой конкретной проблемы, заключается в том, что мое функциональное решение в OCaml было медленнее, чем императивное, тогда как «функциональное» решение в C ++ не подвергалось никаким штрафам по сравнению с аналоговым императивным подходом.
C ++ код был скомпилирован с g++ -std="c++0x" -O3 -o max_cpp max.cpp
, используя gcc-4.6.3
и код OCaml с: ocamlopt -o max_ml max.ml
используя ocamlopt версии 3.12.1.
Оба сгенерированных исполняемых файла были 32-битными и работали на Ubuntu x64 11.04
Я отправляю код обеих программ.
Код C ++ (конечно, не совсем безопасный;))
#include <iostream>
#include <vector>
#include <numeric>
template <typename T> T max (T a, T b) {
return (a>b)? a : b;
}
template <typename I> typename I::value_type find_max (I begin, I end) {
auto max = *begin;
for (begin++;begin!=end;begin++)
if (*begin > max) max = *begin;
return max;
}
template <typename I> typename I::value_type find_max1(I begin, I end) {
return std::accumulate(begin, end, *begin, max< typename I::value_type> );
}
int main(int argc, char ** argv) {
const size_t nElem = atoi(argv[1]);
const size_t nIter = atoi(argv[2]);
std::vector<int> intA(nElem);
std::vector<double> dubA(nElem);
for (int i=0;i<nIter;i++) {
auto res1 = find_max(intA.begin(), intA.end());
auto res2 = find_max(dubA.begin(), dubA.end());
std::cout << "Max int: " << res1 << " " << "Max dub: " << res2 << std::endl;
}
}
Код OCaml
let max a b = if a > b then a else b
(* functional version *)
let find_max vector =
Array.fold_right max vector vector.(0)
(* imperative version *)
let find_max1 vector =
let length = Array.length vector in
let max = ref (vector.(0)) in
for i=1 to length-1 do
if vector.(i) > !max then max := vector.(i)
done; max
let nElems = int_of_string Sys.argv.(1)
let nIter = int_of_string Sys.argv.(2)
let _ = let intA = Array.make nElems 0 in
let dubA = Array.make nElems 0.0 in
for i=1 to nIter do
let res1 = find_max intA in
let res2 = find_max dubA in
print_int !res1;
print_newline ();
print_float !res2;
done
Однако, если я «перегружу» функцию для распознавания целых чисел и чисел с плавающей запятой, то производительность программы даже превосходит производительность моего кода C ++ на 50%! Интересно, почему, хотя.
let find_max_int (vector : int array) =
let length = Array.length vector in
let max = ref (vector.(0)) in
for i=1 to length-1 do
if vector.(i) > !max then max := vector.(i)
done; max
let find_max_float (vector : float array) =
let length = Array.length vector in
let max = ref (vector.(0)) in
for i=1 to length-1 do
if vector.(i) > !max then max := vector.(i)
done; max
Время было выполнено довольно грубо с:
time ./max_cpp 1000000 100
а также time ./max_ml 1000000 100
В OCaml оператор сравнения (<)
является универсальной функцией типа 'a -> 'a -> bool
(аналогично для равенства и т. д.). Это означает, что он реализован с помощью специального примитива в системе времени выполнения, который эффективно выполняет произвольное сравнение для структуры данных любого типа. Средство проверки типов способно оптимизировать мономорфные сравнения по целым числам и выполняет специализированную операцию сравнения вместо выполнения проверки типа во время выполнения в полиморфном случае. Разница в эффективности в десять раз неудивительна, если вы тестируете только эту операцию.
Обратите внимание, что для получения максимальной гибкости вы должны абстрагироваться от операции сравнения в find_max
, Это, однако, предотвратит оптимизацию мономорфизации, поэтому встроенная версия будет работать лучше.
Я думаю, что ваш подход (выполнение микро-бенчмарков и в надежде узнать интересные вещи о производительности реальных программ) ошибочен. Вы столкнулись с очень специфическим случаем поведения производительности OCaml и ошибочно пришли к выводу, что производительность полиморфных функций «плохая». Это явно плохой вывод из преждевременной оптимизации. Напишите действительно представительный пример вычислений, которые вы намереваетесь выполнить, а затем рассмотрите производительность этого реального кода. Вы узнаете очень мало правды из микро-тестов такого рода, и много несущественных деталей.
(Однако верно, что подход к специализации кода C ++ в целом может дать более эффективный код, чем метод компиляции OCaml, который компилирует только одну версию функции для всех типов, но должен компрометировать соответствующие представления данных; в OCaml могут быть издержки связанные с полиморфизмом. Тем не менее, это наблюдается только в довольно специфических случаях, и вы часто можете просто сделать определенный проход вставки в небольшом критическом разделе вашего кода. В результате вы получаете гораздо более быструю компиляцию (без дублирования) и фактически читаемую ошибку сообщения — как вы могли бы иметь, если концепции были интегрированы в C ++.)
редактироватьЯ был на самом деле неправ, говоря, что абстрагирование от сравнения предотвратит оптимизацию с учетом типов. Следующий код, хотя и не такой быстрый, как встроенная от руки версия, все же заметно быстрее, чем версия, использующая полиморфное сравнение:
let find_max1 comp vector =
let length = Array.length vector in
let max = ref (vector.(0)) in
for i=1 to length-1 do
if comp !max vector.(i) then max := vector.(i)
done;
!max
let find_max_int (vector : int array) = find_max1 (fun x y -> x < y) vector
let find_max_float (vector : float array) = find_max1 (fun x y -> x < y) vector
Фактически вы сравниваете специализированные функции во время компиляции с диспетчеризованными во время выполнения. Более адекватный код на стороне ocaml будет использовать функторы, которые теоретически могут уменьшить количество косвенных вызовов до нуля, но я думаю, что он все еще будет страдать от недероптимизации. Другая проблема заключается в том, что представление данных является однородным и не специализированным / встроенным для типа пользователя в любом случае.