Можно, конечно, повысить производительность эта операция значительно. Хорошей новостью является то, что вам не нужно бросаться в Java для этого: Clojure чрезвычайно быстр, если вы оптимизируете его правильно и в большинстве случаев можете создавать ту же скорость, что и чистая Java.
Для достижения максимальной производительности цифрового кода в Clojure вы хотите использовать:
- массивы, потому что вы хотите изменяемое хранение с очень быстрой записью и поиском. Clojure последовательности и векторы красивы, но они приходят с накладными расходами, которые вы, вероятно, хотите избежать для действительно критически важного кода
- double примитивы, потому что они предлагают гораздо более быструю математику.
- aset/aget/areduce - это чрезвычайно быстрые операции, предназначенные для массивов, и в основном дают вам тот же байт-код, что и чистые эквиваленты Java.
- императивный стиль - хотя он является унииоматическим в Clojure, он получает самые быстрые результаты (главным образом потому, что вы можете избежать накладных расходов из распределения памяти, бокса и вызовов функций). Примером может служить dotimes для быстрого императивного цикла.
- (set! * Warning-on-reflection * true) - и устранить любые предупреждения, которые производит ваш код, потому что отражение - большой убийца производительности.
Следующая должно быть в правильном направлении, и, вероятно, получить вам примерно эквивалентную производительности Java:
(def kernel (double-array [0 1 1 2 3 3 0 0 0 0 0 0]))
(def data (double-array [1 5 7 4 8 3 9 5 6 3 2 1 1 7 4 9 3 2 1 8 6 4]))
(defn convolve [^doubles kernel-array ^doubles data-array]
(let [ks (count kernel-array)
ds (count data-array)
output (double-array (+ ks ds))
factor (/ 1.0 (areduce kernel-array i ret 0.0 (+ ret (aget kernel-array i))))]
(dotimes [i (int ds)]
(dotimes [j (int ks)]
(let [offset (int (+ i j))]
(aset output offset (+ (aget output offset) (* factor (* (aget data-array i) (aget kernel-array j))))))))
output))
(seq (convolve kernel data))
=> (0.0 0.1 0.6 1.4 2.4 4.4 5.5 6.1000000000000005 5.600000000000001 6.200000000000001 5.499999999999999 5.9 4.199999999999999 3.3000000000000003 2.5 2.2 3.3 4.4 5.6000000000000005 4.8 4.8999999999999995 3.1 3.5 4.300000000000001 5.0 3.0 1.2000000000000002 0.0 0.0 0.0 0.0 0.0 0.0 0.0)
Я не обрезается выходной массив или сделать какой-либо ограничивающего так что вам, возможно, потребуется немного взломать это решение, чтобы получить именно тот результат, который вы хотите, но, надеюсь, вы получите эту идею .....
Некоторые очень грубо бенчмаркинг:
(time (dotimes [i 1000] (seq (convolve kernel data))))
=> "Elapsed time: 8.174109 msecs"
то это примерно 30 нс в комбинации пар ядро / данных - Я полагаю, что это в значительной степени поражая границы кэшированных доступа к памяти.
фантастический ответ, очень подробные комментарии. Что такое '' удваивает вещь в 'defn'? И для того, чтобы это стало понятным для меня, вы имеете в виду, что мне нужно как можно ближе подойти к тому, как я это сделал бы в java, не так ли? Я имею в виду, в основном, вложенные петли 'for'. – Ali
^doubleles - это тип подсказки, указывающий примитивный двойной массив. это помогает компилятору оптимизировать методы aget. и да, вложенные для циклов и мутации массива - это самый быстрый способ решить эту конкретную проблему. Не очень Clojure-y, но он работает! – mikera
Удивительная вещь для меня заключается в том, что это не намного быстрее, чем простой подход HOF ниже, особенно при включении '* unchecked-math *' (новый в 1.3). –