23

我目前正在研究 Clojure 和 Incanter 作为 R 的替代品。(不是我不喜欢 R,但尝试新语言很有趣。)我喜欢 Incanter 并且发现语法很吸引人,但相比而言,矢量化操作相当慢例如到 R 或 Python。

例如,我想使用 Incanter 向量运算、Clojure map 和 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"

回复:

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 中的向量操作?还欢迎使用 Clojure 中的循环、Java 数组和/或库的解决方案。

我也将这个问题发布到 Incanter Google 组,到目前为止没有任何回应。

更新:我已将 Jouni 的答案标记为已接受,请参阅下面的我自己的答案,我已经清理了他的代码并添加了一些基准。

4

5 回答 5

20

我的最终解决方案

在所有测试之后,我发现了两种稍微不同的方法来以足够的速度进行计算。

首先,我使用了diff具有不同类型返回值的函数,下面是返回向量的代码,但我还计时了一个返回双数组(用 y 替换 (vec y) )和 Incanter.matrix(替换 ( vec y) 与矩阵 y)。此函数仅基于 java 数组。这是基于 Jouni 的代码,删除了一些额外的类型提示。

另一种方法是使用 Java 数组进行计算并将值存储在瞬态向量中。正如您从时序中看到的那样,如果您不希望函数返回和排列,这比方法 1 稍快。这是在函数中实现的difft

所以选择真的取决于你不想对数据做什么。我想一个不错的选择是重载函数,以便它返回调用中使用的相同类型。实际上将 java 数组传递给 diff 而不是向量会使 ~1s 更快。

不同功能的时序:

差异返回向量:

(time (def y (diff x)))
"Elapsed time: 4733.259 msecs"

返回 Incanter.matrix 的差异:

(time (def y (diff x)))
"Elapsed time: 2599.728 msecs"

diff 返回双数组:

(time (def y (diff x)))
"Elapsed time: 1638.548 msecs"

差异:

(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))) 
于 2010-09-29T12:15:44.633 回答
14

这是一个 Java 数组实现,它在我的系统上比您的 R 代码 (YMMV) 更快。注意启用反射警告,这在优化性能时是必不可少的,以及 y 上的重复类型提示(def 上的那个似乎对 aset 没有帮助)并将所有内容转换为原始双精度值(dotimes 确保i 是原始 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))))))))
于 2010-09-28T20:29:55.977 回答
2

Bradford Cross 的博客有很多关于此的帖子(他将这些东西用于他在链接文本上工作的启动。一般来说,在内部循环中使用瞬态,类型提示(通过*warn-on-reflection*)等都有助于提高速度。Clojure 的乐趣有一个很好的关于性能调整的部分,你应该阅读。

于 2010-09-28T19:38:52.770 回答
1

这是一个瞬态的解决方案 - 有吸引力但缓慢。

(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)))))))))
于 2010-09-29T14:46:31.933 回答
0

到目前为止,所有评论都是由似乎没有太多加速 Clojure 代码经验的人提出的。如果您希望 Clojure 代码执行与 Java 相同的功能 - 可以使用这些工具来执行此操作。然而,对于向量数学,推迟使用 Colt 或 Parallel Colt 等成熟的 Java 库可能更有意义。将 Java 数组用于绝对最高性能的迭代可能是有意义的。

@Shane 的链接充满了过时的信息,几乎不值得一看。@Shane 的评论说代码比 10 倍慢是完全不准确的(并且不受支持http://shootout.alioth.debian.org/u32q/compare.php?lang=clojure,这些基准不考虑1.2.0 或 1.3.0-alpha1) 中可能进行的各种优化。通过一些工作,通常很容易获得 4X-5X 的 Clojure 代码。除此之外,通常需要对 Clojure 的快速路径有更深入的了解——由于 Clojure 是一门相当年轻的语言,因此没有得到广泛传播。

Clojure 非常快。但是学习如何让它变得更快需要一些工作/研究,因为 Clojure 不鼓励可变操作和可变数据结构。

于 2010-09-28T17:40:26.490 回答