Быстрая векторная математика в Clojure/Incanter
В настоящее время я рассматриваю Clojure и Incanter как альтернативу Р. (Не то, чтобы мне не нравилось R, но просто интересно попробовать новые языки.) Мне нравится Incanter и найти синтаксис, привлекательный, но векторизованные операции довольно медленны по сравнению, например на R или Python.
В качестве примера я хотел получить разность первого порядка для вектора
с использованием векторных операций Incanter, карты Clojure и R. Ниже приведен код и время для всех
версии. Как видно, R явно быстрее.
Incanter и Clojure:
(use '(incanter core stats))
(def x (doall (sample-normal 1e7)))
(time (def y (doall (minus (rest x) (butlast x)))))
"Elapsed time: 16481.337 msecs"
(time (def y (doall (map - (rest x) (butlast x)))))
"Elapsed time: 16457.850 msecs"
R:
rdiff <- function(x){
n = length(x)
x[2:n] - x[1:(n-1)]}
x = rnorm(1e7)
system.time(rdiff(x))
user system elapsed
1.504 0.900 2.561
Итак, мне было интересно, есть ли способ ускорить векторные операции в Incanter/ Clojure? Также приветствуются решения, связанные с использованием циклов, массивов Java и/или библиотек из Clojure.
Я также разместил этот вопрос для группы Incanter Google без ответов.
ОБНОВЛЕНИЕ: Я отметил ответ Jouni как принятый, см. ниже мой собственный ответ, где я немного очистил его код и добавил некоторые тесты.
Ответы
Ответ 1
Здесь реализация Java-массивов, которая находится в моей системе быстрее, чем ваш R-код (YMMV). Примечание, позволяющее использовать предупреждения отражения, которые необходимы при оптимизации производительности, а также повторяющийся тип подсказки на y (тот, который на def не помог для aset) и отбросил все до примитивных двойных значений (dotimes гарантирует, что я является примитивным int).
(set! *warn-on-reflection* true)
(use 'incanter.stats)
(def ^"[D" x (double-array (sample-normal 1e7)))
(time
(do
(def ^"[D" y (double-array (dec (count x))))
(dotimes [i (dec (count x))]
(aset ^"[D" y
i
(double (- (double (aget x (inc i)))
(double (aget x i))))))))
Ответ 2
Мои окончательные решения
После всего тестирования я нашел два немного разных способа сделать расчет с достаточной скоростью.
Сначала я использовал функцию diff
с различными типами возвращаемых значений, ниже приведен код, возвращающий вектор, но я также приурочил версию, возвращающую двойной массив (replace (vec y) с y) и Incanter.matrix(замените (vec y) на матрицу y). Эта функция основана только на массивах java. Это основано на коде Jouni с удалением некоторых дополнительных типов.
Другим подходом является выполнение вычислений с массивами Java и сохранение значений в переходном векторе. Как видно из таймингов, это немного быстрее, чем подход 1, если вы не хотите возвращать функцию и массивы. Это реализовано в функции difft
.
Таким образом, выбор действительно зависит от того, что вам не нужно делать с данными. Я думаю, что хорошим вариантом было бы перегрузить функцию так, чтобы она возвращала тот же тип, который использовался в вызове. Фактически передача массива java в diff вместо вектора делает ~ 1 с быстрее.
Сроки для разных функций:
diff возвращающий вектор:
(time (def y (diff x)))
"Elapsed time: 4733.259 msecs"
diff return Incanter.matrix:
(time (def y (diff x)))
"Elapsed time: 2599.728 msecs"
diff возвращающий двойной массив:
(time (def y (diff x)))
"Elapsed time: 1638.548 msecs"
difft:
(time (def y (difft x)))
"Elapsed time: 3683.237 msecs"
Функции
(use 'incanter.stats)
(def x (vec (sample-normal 1e7)))
(defn diff [x]
(let [y (double-array (dec (count x)))
x (double-array x)]
(dotimes [i (dec (count x))]
(aset y i
(- (aget x (inc i))
(aget x i))))
(vec y)))
(defn difft [x]
(let [y (vector (range n))
y (transient y)
x (double-array x)]
(dotimes [i (dec (count x))]
(assoc! y i
(- (aget x (inc i))
(aget x i))))
(persistent! y)))
Ответ 3
Не определен для вашего примера кода, но так как это превратилось в обсуждение производительности Clojure, вам может понравиться эта ссылка:
Clojure Быстро
Ответ 4
Блог Bradford Cross содержит кучу сообщений об этом (он использует этот материал для запуска, который он работает на текст в тексте. В общем, использование переходных процессов во внутренних циклах, тип подсказки (через *warn-on-reflection*
) и т.д. - все это полезно для увеличения скорости. Радость Clojure имеет отличный раздел на настройке производительности, которую вы должны прочитать.
Ответ 5
Здесь решение с переходными процессами - привлекательно, но медленно.
(use 'incanter.stats)
(set! *warn-on-reflection* true)
(def x (doall (sample-normal 1e7)))
(time
(def y
(loop [xs x
xs+ (rest x)
result (transient [])]
(if (empty? xs+)
(persistent! result)
(recur (rest xs) (rest xs+)
(conj! result (- (double (first xs+))
(double (first xs)))))))))
Ответ 6
Все комментарии пока есть у людей, у которых, похоже, нет большого опыта, ускоряющего код Clojure. Если вы хотите, чтобы код Clojure выполнялся идентично Java - средства доступны для этого. Тем не менее, может быть больше смысла откладывать на зрелые Java-библиотеки, такие как Colt или Parallel Colt для векторной математики. Может иметь смысл использовать массивы Java для абсолютной итерации с наивысшей производительностью.
Ссылка @Shane настолько заполнена устаревшей информацией, что ее вряд ли стоит посмотреть. Также @Shane комментирует, что код медленнее, чем в 10 раз, является просто неточным (и не поддерживается http://shootout.alioth.debian.org/u32q/compare.php?lang=clojure, и эти тесты не учитывают возможные варианты оптимизации в 1.2.0 или 1.3.0-alpha1). С небольшим количеством работы обычно легко получить код Clojure с 4X-5X. Помимо этого, как правило, требуется более глубокое знание быстрых путей Clojure - что-то широко не распространяется, поскольку Clojure - довольно молодой язык.
Clojure довольно быстро. Но изучение того, как быстро сделать это, займет немного работы/исследования, поскольку Clojure обескураживает изменяемые операции и изменяемые структуры данных.