Производительность стека в языках программирования - PullRequest
8 голосов
/ 08 ноября 2010

Ради интереса я попытался сравнить производительность стека пары языков программирования, вычисляя ряд Фибоначчи с использованием наивного рекурсивного алгоритма. Код в основном одинаков на всех языках, я выложу 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-компиляцией?

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

РЕДАКТИРОВАТЬ: спасибо за интересные ответы! Я признаю, что это неправильный тест (никогда не говорил, что это был: P), и, возможно, я смогу сделать лучший тест и опубликовать его вам в следующий раз, в свете того, что мы обсудили:)

РЕДАКТИРОВАТЬ 2: я обновил время выполнения реализации ocaml, используя оптимизирующий компилятор ocamlopt. Также я опубликовал тестовый стенд на https://github.com/hoheinzollern/fib-test. Не стесняйтесь вносить в него дополнения, если хотите:)

Ответы [ 8 ]

17 голосов
/ 08 ноября 2010

Возможно, вы захотите повысить уровень оптимизации вашего компилятора Си.С 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;
}

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

11 голосов
/ 08 ноября 2010

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

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

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

let () = Format.printf "%d@." (f 40)


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

real    0m1.643s

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

4 голосов
/ 08 ноября 2010

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

Что касается производительности Java, то это удивительно.Редко Java обгоняет C (даже при очень небольшой оптимизации компилятора на стороне C) ... что JIT может делать - это запоминание или хвостовая рекурсия ...

4 голосов
/ 08 ноября 2010

Одна возможность состоит в том, что компилятор 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 ... может быть интересно поместить внешний вызов в цикл, чтобы получить прогоны в минуту или около того.)

2 голосов
/ 08 ноября 2010

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

1 голос
/ 08 ноября 2010

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

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

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

1 голос
/ 08 ноября 2010

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

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

0 голосов
/ 12 ноября 2010

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...