2010-11-08 4 views
8

Просто для удовольствия я попытался сравнить производительность стека с несколькими языками программирования, вычисляющими ряды Фибоначчи, используя наивный рекурсивный алгоритм. Код в основном одинаковы во всех языках, я выложу версию Java:Производительность стека в языках программирования

public class Fib { 
public static int fib(int n) { 
    if (n < 2) return 1; 
    return fib(n-1) + fib(n-2); 
} 

public static void main(String[] args) { 
    System.out.println(fib(Integer.valueOf(args[0]))); 
} 
} 

ИТАК точка в том, что с помощью этого алгоритма с входом 40 я получил эти тайминги:

C: 2.796s 
Ocaml: 2.372s 
Python: 106.407s 
Java: 1.336s 
C#(mono): 2.956s 

Они взяты в поле Ubuntu 10.04, используя версии каждого языка, доступные в официальных репозиториях, на двухъядерном процессоре Intel.

Я знаю, что функциональные языки, такие как ocaml, имеют замедление, которое происходит от обработки функций в качестве граждан первого порядка и не имеют проблем объяснять время работы CPython из-за того, что это единственный интерпретируемый язык в этом тесте, но я был впечатлен по времени java, которое составляет половину c для одного и того же алгоритма! Не могли бы вы приписать это компиляции JIT?

Как вы объясните эти результаты?

EDIT: спасибо за интересные ответы! Я признаю, что это не правильный тест (он никогда не говорил, что это: P), и, может быть, я смогу сделать лучшее и отправить его вам в следующий раз, в свете того, что мы обсудили :)

EDIT 2 : Я обновил среду выполнения ocaml, используя оптимизирующий компилятор ocamlopt. Также я опубликовал стенд на https://github.com/hoheinzollern/fib-test. Не стесняйтесь делать дополнения к нему, если хотите :)

+4

Помимо обычных правил, применяемых к эталонам ... (1) Компилятор OCaml * (native) * довольно агрессивен и не должен быть в шесть раз медленнее C, имея дело с такой важной концепцией FP как рекурсия. Вы использовали интерпретатор байт-кода? (2) Какие параметры оптимизации для C? – delnan

+11

Вы выполнили несколько образцов? Вы удалили выбросы? Получили ли вы средние результаты? Вы измеряли время или время процессора? Вы даже заметили статистику? :-) – paxdiablo

+2

То, что меня удивляет, - это время работы java. Я видел это раньше ... делал метод Quicksort на C и Java, а Java превосходил C каждый раз. – Nicholas

ответ

4

Одна из возможностей заключается в том, что компилятор C оптимизируется на предположение, что первая ветвь (n < 2) является еще более часто используемой. Это нужно делать только во время компиляции: сделать предположение и придерживаться его.

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

Вы может быть в состоянии увидеть разницу путем переворачивания логики if:

public static int fib(int n) { 
if (n >= 2) return fib(n-1) + fib(n-2); 
return 1; 
} 

Это стоит попробовать, так или иначе :)

Как всегда, проверьте настройки оптимизации для всех платформ. Очевидно, что настройки компилятора для C - и на Java, попробуйте использовать клиентскую версию Hotspot и версию сервера. (Обратите внимание, что вам нужно работать более секунды или около того, чтобы действительно получить полную выгоду от Hotspot ... может быть интересно поставить внешний вызов в цикле, чтобы получить прогон минуты или около того.)

+0

Изменение порядка ветвления в коде c не влияет на производительность.Должна быть какая-то другая оптимизация, которую я не могу понять сейчас. – hoheinzollern

+0

Кроме того, почему реализация C# в два раза медленнее? (пытаясь с .net, я получаю те же результаты, это не ошибка моно) – hoheinzollern

+0

@hoheinzollem: Не знаю. Имейте в виду, что это такой маленький тест, что это может быть легко, когда команда Java сделала компромисс в одну сторону, команда .NET сделала компромисс в обратном порядке, и в этом конкретном случае подход Java работает хорошо. –

11

Вы говорите, что очень мало о конфигурации (бенчмаркинга, детали все: commandlines, компьютер, используемый, ...)

Когда я пытаюсь воспроизвести для OCaml я получаю:

let rec f n = if n < 2 then 1 else (f (n-1)) + (f (n-2)) 

let() = Format.printf "%[email protected]" (f 40) 


$ ocamlopt fib.ml 
$ time ./a.out 
165580141 

real 0m1.643s 

Это находится на Intel Xeon 5150 (Core 2) на частоте 2,66 ГГц. Если я использую байт-код OCaml-компилятор ocamlc, с другой стороны, я получаю время, подобное вашему результату (11s).Но, конечно, для выполнения сравнения скорости нет причин использовать компилятор байт-кода, если вы не хотите сравнивать скорость самой компиляции (ocamlc поражает скоростью компиляции).

+2

Я * знал * это! :) – delnan

+1

хороший, я не знал, что есть два компилятора ocaml, и на самом деле я использовал байт-код один .. спасибо за объяснение! – hoheinzollern

+2

@hoheinzollern Есть четыре! Там также 'ocamlc.opt', компилятор, генерирующий байт-код, скомпилированный с' ocamlopt' и 'ocamlopt.opt', собственный компилятор, скомпилированный с собой. Разумеется, эти два кода производят тот же код, что и их совместимые с байткодом версии. –

17

Возможно, вы захотите проверить уровень оптимизации вашего компилятора C. С gcc -O3, что имеет большое значение, падение с 2,015 секунды до 0,766 секунды, что составляет около 62%.

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

Кроме того, убедитесь, что вы измеряете время процессора, а не время часов.

Что-то меньшее, чем я, я бы не счел приемлемым статистический анализ, и он вполне может подвергнуться шуму, делая ваши результаты бесполезными.

Для чего это необходимо, эти тайминги C были для семи трасс с выбросами, выведенными до усреднения.


Фактически, этот вопрос показывает, насколько важен выбор алгоритма, нацеленный на высокую производительность. Хотя рекурсивные решения, как правило, элегантны, этот страдает от недостатка, что вы дублируете расчеты . Итеративный версия:

int fib(unsigned int n) { 
    int t, a, b; 
    if (n < 2) return 1; 
    a = b = 1; 
    while (n-- >= 2) { 
     t = a + b; 
     a = b; 
     b = t; 
    } 
    return b; 
} 

дополнительно снижается время, необходимое, от 0,766 до 0,078 секунды секунд, дальнейшее сокращение на 89% и снижение на необычайные 96% от исходного кода.


И, как последняя попытка, вы должны попробовать следующее, который сочетает в себе справочную таблицу с расчетами за пределами определенной точки:

static int fib(unsigned int n) { 
    static int lookup[] = { 
     1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 
     610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 
     46368, 75025, 121393, 196418, 317811, 514229, 832040, 
     1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 
     24157817, 39088169, 63245986, 102334155, 165580141 }; 
    int t, a, b; 

    if (n < sizeof(lookup)/sizeof(*lookup)) 
     return lookup[n]; 
    a = lookup[sizeof(lookup)/sizeof(*lookup)-2]; 
    b = lookup[sizeof(lookup)/sizeof(*lookup)-1]; 
    while (n-- >= sizeof(lookup)/sizeof(*lookup)) { 
     t = a + b; 
     a = b; 
     b = t; 
    } 

    return b; 
} 

Это сокращает время еще раз, но я подозреваю, что мы «Здесь мы сталкиваемся с уменьшением прибыли.

+1

Ahem, значения являются только выбросами, если они больше, чем '1.5 * (Q3 - Q1)' вдали от первого или третьего квартили ... –

+0

Ну, это определение _one_ и, вероятно, хорошее, но я не верю, что есть a _rigid_ в статистике. У меня нет проблем с использованием упрощенного определения, если большинство точек данных будут находиться в разумном диапазоне. YMMV. – paxdiablo

+1

Вы можете использовать другой стандарт для выхода, если хотите, но вы должны использовать стандарт. Удаление самых быстрых и самых медленных, безусловно, не является стандартом. – Falmarri

2

Обратите внимание, что если виртуальная точка Java Hotspot достаточно умна для memoise вызовов fib(), она может сократить экспоненциальную стоимость алгоритма примерно до линейной стоимости.

+3

Я достаточно умен, чтобы «memoize» 'fib' звонки тоже. Я заменяю их на 'static const int fib [] = {1, 1, 2, 3, ...};', который меньше любой возможной реализации в коде для диапазона 'int'. –

+0

Но если JVM использует скользящее окно или что-то для напоминания последних N вызовов для fib(), это будет превосходить вашу статическую таблицу поиска. – user268396

+1

@ user268396: № R .. нужен только массив из 47 номеров фибоначчи. 48-й переполняет 32-битный int. – JeremyP

4

Я могу объяснить производительность Python. Производительность Python для рекурсии в лучшем случае ужасна, и ее следует избегать, как чуму при кодировании в ней. Тем более, что переполнение стека происходит по умолчанию при глубине рекурсии только 1000 ...

Что касается производительности Java, это потрясающе. Редко, что Java бьет C (даже при очень небольшой оптимизации компилятора на стороне C) ... что JIT может делать, это memoization или tail recursion ...

+0

Nitpicking: предел рекурсии CPython по умолчанию равен 1000, по крайней мере, на обычных архитектурах настольных компьютеров. В противном случае, да. – delnan

+2

Да, рекурсия не подходит для Python, особенно когда вы идете вглубь. Я решил попробовать как с Psyco (JIT-компилятор), так и с Cython. Psyco сократил время с 75,6 до 3,36 секунды. С Cython я смог довести его до 1,27. Если времена действительно пропорциональны значениям в OP, тогда код Cython будет работать около 1,79 секунды на его машине. Это довольно прилично по моим стандартам, но, возможно, времена не будут столь масштабными. –

+0

@ delnan: он варьируется от ОС до ОС и версии до версии, но да, он появляется на компьютере, на котором я сейчас нахожусь 1000. –

1

С помощью C вы должны либо объявить функцию фибоначчи " inline "или, используя gcc, добавьте аргумент -finline-functions к параметрам компиляции. Это позволит компилятору выполнять рекурсивную вставку. Это также причина, почему с -O3 вы получаете лучшую производительность, она активирует -finline-functions.

Редактировать Вам необходимо хотя бы указать -O/-O1, чтобы иметь рекурсивную вставку, также если функция объявлена ​​встроенной.На самом деле, тестируя себя, я обнаружил, что объявляя функцию inline и используя -O в качестве флага компиляции или просто используя -O -finline-functions, мой рекурсивный код фибоначчи был быстрее, чем с -O2 или -O2 -finline-functions.

1

Я написал версию наивной функции Фибоначчи на С и скомпилировал ее на ассемблере в gcc (4.3.2 Linux). Затем я скомпилировал его с помощью gcc-O3.

Неоптимизированная версия была длиной 34 строки и выглядела как прямой перевод кода C.

Оптимизированная версия была длиной 190 строк и (это было трудно сказать, но) она оказалась инлайн по крайней мере, звонки для значений до около 5.

0

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

Попробуйте приблизиться к размеру стека, который вам нужен, и заставить компоновщик выделить столько пространства стека. Затем отключите проверку стека и переустановите программу.