【问题标题】:Fast vector math in Clojure / IncanterClojure / Incanter 中的快速向量数学
【发布时间】:2026-01-23 02:20:05
【问题描述】:

我目前正在研究 Clojure 和 Incanter 作为 R 的替代品。(不是我不喜欢 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 中的向量操作?也欢迎使用 Clojure 中的循环、Java 数组和/或库的解决方案。

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

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

【问题讨论】:

  • 这会很困难,因为 R 的内在循环是用 C 或 Fortran 编程的。变得比这更快需要相当多的努力......
  • 这和我之前的经验是一致的; Clojure 在基本操作上的速度几乎慢了 10 倍。我的建议:如果您正在寻找性能,请不要使用 Clojure;如果您想在 JVM 等上进行无缝集成,请使用它。您可能还会发现这个问题相关:*.com/questions/2186709/…
  • rdiff 最好写成x[-1] - x[-n]
  • diff(已经存在)。例如,{ tmp &lt;- rnorm(1e7); all(diff(tmp) == (tmp[-1]-tmp[-length(tmp)])) } #--> TRUE.
  • @Matti 我可以理解你想比较相似的代码,但如果我比较语言,我会使用每种语言中最好的工具。就个人而言,我喜欢从柠檬中挤出最后一滴。

标签: r vector clojure incanter


【解决方案1】:

我的最终解决方案

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

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

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

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

不同功能的时间安排:

diff 返回向量:

(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))) 

【讨论】:

  • 这是伟大的工作。完成后,您应该将您的函数发送回 Incanter 邮件列表,以便将其包含在其中...
  • 很抱歉,有人对 Clojure 的性能发表声明而没有提出任何声明的经验,这让我感到沮丧。请将此答案标记为正确答案。另一个会鼓励人们使用不必要的类型提示。
  • 根据官方文档“瞬态不是为就地攻击而设计的”。你应该使用assoc!,就像它自然的assoc一样。所以你的最后几行应该看起来像这样(persistent! (areduce (assoc! ...) x)) - 而不是dotimes
【解决方案2】:

这是一个 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))))))))

【讨论】:

  • 谢谢,您的代码在我的系统上也比 R 运行得更快(花了 400 毫秒),但是如果从 x 作为 Clojure 向量开始计时并使用“vec”将 y 也转换回向量" 命令它实际上总共需要(从输入向量到输出向量)4.5 秒以上。您的解决方案仍然比原始解决方案快约 3 倍,并且更接近 R!我特别欣赏这样一个关于类型提示的好例子,因为这是我一直在努力解决的问题。
  • 请注意,核心团队正在努力在 Clojure 1.3 中实现更好的数字性能。 combinate.us/clojure/2010/09/27/clojure
  • @Matti Pastell:看看瞬变,它们应该加快从数组到向量的转换速度。 clojure.org/transients
  • 也许您可以详细说明为什么您要使用完全向量?数组是“可序列化的”,这意味着通常的序列操作可以很好地处理它们。
  • 当然 get 有可能更快,因为它避免了数组访问之前的一些类型检查以及可能的原语装箱,但唯一知道的方法是在实际上下文中测量它你想用它。 JIT 可能会简化许多类型检查。
【解决方案3】:

Bradford Cross's blog 有很多关于此的帖子(他将这些东西用于他在link text 工作的初创公司。一般来说,在内部循环中使用瞬态,类型提示(通过*warn-on-reflection*)等都适合速度提高。Clojure 的乐趣中有一个关于性能调整的精彩部分,您应该阅读。

【讨论】:

  • 谢谢,我注意到他还编写了 Infer 库 (github.com/bradford/infer),其中包括一些使用 UJMP (ujmp.org) 的相当快的向量运算。不幸的是,从 Clojure 向量到 infer.matrix 的转换非常慢(1e7 大约 30 秒)。
【解决方案4】:

这是一个带有瞬变的解决方案 - 很吸引人,但速度很慢。

(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)))))))))

【讨论】:

  • 我已将我的解决方案更新为瞬态和 get 的混合,它比使用 aset 存储结果并转换为向量略快。请注意,使用“assoc!”在预先分配的向量上比“conj!”更快。
【解决方案5】:

到目前为止,所有 cmets 都是由似乎没有太多加速 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 不鼓励可变操作和可变数据结构。

【讨论】:

  • 谢谢,你能给我一个使用 Clojure 的 Parallel Colt 的例子吗?我知道 Incanter 在某些事情上使用 Parallel Colt,但我猜不是向量数学,因为它太慢了。
  • 语言大战 (a) 不包括 R 并且 (b) 不一定解决 Matti 正在考虑的问题类型(即向量化操作)。如果基准测试问题已经过时,您为什么不帮我们大家一个忙并更新它!
  • >> 这些基准没有考虑 1.2.0 中可能的优化类型
  • 我在您的帖子中没有看到任何建议。 Shanes 链接实际上是不断更新的,并且仍然包含很多有效信息,由他和其他人提供。保持公平,好吗?如果他没有任何正当的论据,乔本华也会诉诸“那完全是假的”。他实际上把它变成了一种艺术,但我不喜欢在讨论中看到它。可以说,它更多地是关于演讲者而不是关于主题......