Быстрая сложная арифметика чисел в Clojure
Я реализовал некоторую базовую сложную арифметику чисел в Clojure и заметил, что она примерно в 10 раз медленнее, чем примерно эквивалентный Java-код, даже с подсказками типов.
Для сравнения:
(defn plus [[^double x1 ^double y1] [^double x2 ^double y2]]
[(+ x1 x2) (+ y1 y2)])
(defn times [[^double x1 ^double y1] [^double x2 ^double y2]]
[(- (* x1 x2) (* y1 y2)) (+ (* x1 y2) (* y1 x2))])
(time (dorun (repeatedly 100000 #(plus [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(times [1 0] [0 1]))))
выход:
"Elapsed time: 69.429796 msecs"
"Elapsed time: 72.232479 msecs"
с:
public static void main( String[] args ) {
double[] z1 = new double[] { 1, 0 };
double[] z2 = new double[] { 0, 1 };
double[] z3 = null;
long l_StartTimeMillis = System.currentTimeMillis();
for ( int i = 0; i < 100000; i++ ) {
z3 = plus( z1, z2 ); // assign result to dummy var to stop compiler from optimising the loop away
}
long l_EndTimeMillis = System.currentTimeMillis();
long l_TimeTakenMillis = l_EndTimeMillis - l_StartTimeMillis;
System.out.format( "Time taken: %d millis\n", l_TimeTakenMillis );
l_StartTimeMillis = System.currentTimeMillis();
for ( int i = 0; i < 100000; i++ ) {
z3 = times( z1, z2 );
}
l_EndTimeMillis = System.currentTimeMillis();
l_TimeTakenMillis = l_EndTimeMillis - l_StartTimeMillis;
System.out.format( "Time taken: %d millis\n", l_TimeTakenMillis );
doNothing( z3 );
}
private static void doNothing( double[] z ) {
}
public static double[] plus (double[] z1, double[] z2) {
return new double[] { z1[0] + z2[0], z1[1] + z2[1] };
}
public static double[] times (double[] z1, double[] z2) {
return new double[] { z1[0]*z2[0] - z1[1]*z2[1], z1[0]*z2[1] + z1[1]*z2[0] };
}
выход:
Time taken: 6 millis
Time taken: 6 millis
На самом деле, подсказки типа, похоже, не имеют значения: если я их удалю, я получаю примерно такой же результат. Что действительно странно, если я запускаю Clojure script без REPL, я получаю более медленные результаты:
"Elapsed time: 137.337782 msecs"
"Elapsed time: 214.213993 msecs"
Итак, мои вопросы: как я могу приблизиться к производительности Java-кода? И почему на Земле выражения занимают больше времени, чтобы оценить при запуске Clojure без REPL?
ОБНОВЛЕНИЕ ==============
Отлично, используя deftype
с подсказками типов в deftype
и defn
s, а с помощью dotimes
, а не repeatedly
дает производительность так же хорошо, как или лучше, чем версия Java. Спасибо вам обоим.
(deftype complex [^double real ^double imag])
(defn plus [^complex z1 ^complex z2]
(let [x1 (double (.real z1))
y1 (double (.imag z1))
x2 (double (.real z2))
y2 (double (.imag z2))]
(complex. (+ x1 x2) (+ y1 y2))))
(defn times [^complex z1 ^complex z2]
(let [x1 (double (.real z1))
y1 (double (.imag z1))
x2 (double (.real z2))
y2 (double (.imag z2))]
(complex. (- (* x1 x2) (* y1 y2)) (+ (* x1 y2) (* y1 x2)))))
(println "Warm up")
(time (dorun (repeatedly 100000 #(plus (complex. 1 0) (complex. 0 1)))))
(time (dorun (repeatedly 100000 #(times (complex. 1 0) (complex. 0 1)))))
(time (dorun (repeatedly 100000 #(plus (complex. 1 0) (complex. 0 1)))))
(time (dorun (repeatedly 100000 #(times (complex. 1 0) (complex. 0 1)))))
(time (dorun (repeatedly 100000 #(plus (complex. 1 0) (complex. 0 1)))))
(time (dorun (repeatedly 100000 #(times (complex. 1 0) (complex. 0 1)))))
(println "Try with dorun")
(time (dorun (repeatedly 100000 #(plus (complex. 1 0) (complex. 0 1)))))
(time (dorun (repeatedly 100000 #(times (complex. 1 0) (complex. 0 1)))))
(println "Try with dotimes")
(time (dotimes [_ 100000]
(plus (complex. 1 0) (complex. 0 1))))
(time (dotimes [_ 100000]
(times (complex. 1 0) (complex. 0 1))))
Вывод:
Warm up
"Elapsed time: 92.805664 msecs"
"Elapsed time: 164.929421 msecs"
"Elapsed time: 23.799012 msecs"
"Elapsed time: 32.841624 msecs"
"Elapsed time: 20.886101 msecs"
"Elapsed time: 18.872783 msecs"
Try with dorun
"Elapsed time: 19.238403 msecs"
"Elapsed time: 17.856938 msecs"
Try with dotimes
"Elapsed time: 5.165658 msecs"
"Elapsed time: 5.209027 msecs"
Ответы
Ответ 1
Вероятными причинами низкой производительности являются:
- Clojure векторы являются по существу более тяжелыми структурами данных, чем массивы Java double []. Таким образом, у вас есть довольно много лишних накладных расходов при создании и чтении векторов.
- Вы бокс удваивается как аргументы для ваших функций, а также когда они помещаются в векторы. Бокс /unboxing относительно дорог в этом типе низкоуровневого численного кода.
- Тип подсказок (
^double
) не помогает вам: хотя вы можете иметь примитивные типы подсказок для обычных функций Clojure, они не будут работать на векторах.
Подробнее см. в этом блоге об ускорении примитивной арифметики.
Если вам действительно нужны быстрые сложные номера в Clojure, вам, вероятно, потребуется реализовать их с помощью deftype
, что-то вроде:
(deftype Complex [^double real ^double imag])
И затем определите все ваши сложные функции, используя этот тип. Это позволит вам использовать примитивную арифметику повсюду и должно быть примерно эквивалентно производительности хорошо написанного кода Java.
Ответ 2
-
Я не очень разбираюсь в тестовых тестах, но кажется, что вам нужно
для разогрева jvm, когда вы начинаете тест. Поэтому, когда вы делаете это в REPL, он уже разогревается. Когда вы запустите script, он еще не создан.
-
В java вы запускаете все циклы внутри 1 метода. Вызывается другой метод, кроме plus
и times
. В clojure вы создаете анонимную функцию и вызываете ее повторно для ее вызова. Это занимает некоторое время. Вы можете заменить его на dotimes
.
Моя попытка:
(println "Warm up")
(time (dorun (repeatedly 100000 #(plus [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(times [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(plus [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(times [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(plus [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(times [1 0] [0 1]))))
(println "Try with dorun")
(time (dorun (repeatedly 100000 #(plus [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(times [1 0] [0 1]))))
(println "Try with dotimes")
(time (dotimes [_ 100000]
(plus [1 0] [0 1])))
(time (dotimes [_ 100000]
(times [1 0] [0 1])))
Результаты:
Warm up
"Elapsed time: 367.569195 msecs"
"Elapsed time: 493.547628 msecs"
"Elapsed time: 116.832979 msecs"
"Elapsed time: 46.862176 msecs"
"Elapsed time: 27.805174 msecs"
"Elapsed time: 28.584179 msecs"
Try with dorun
"Elapsed time: 26.540489 msecs"
"Elapsed time: 27.64626 msecs"
Try with dotimes
"Elapsed time: 7.3792 msecs"
"Elapsed time: 5.940705 msecs"