【问题标题】:Optimizing array mutation in Clojure在 Clojure 中优化数组突变
【发布时间】:2012-04-17 22:21:41
【问题描述】:

阅读了几篇关于该主题的博文后,我发现在 Clojure 中对数组进行变异是这样的:

(defn m [xs ys] 
  (dotimes [i (count xs)] 
    (aset #^ints ys (int i) 
    (int (* (int 3) (int (aget #^ints xs (int i))))))))

(def xs (into-array Integer/TYPE (range 1000000)))(def ys (into-array Integer/TYPE (range 1000000))) 的位置

根据 Criterium 平均需要 14 毫秒,而 Java 也是如此,

public static int[] m(int[] x, int[] y)
{
  for(int i=0; i<x.length; i++)
    y[i] = 3*x[i];
  return y;
}

平均需要 800us。 **

我是否正在尽我所能让事情进展得更快?还有什么我可以沿着优化路径走下去的吗?

** 我使用 Criterium 和 (report-result (bench (m xs ys )) :verbose)(report-result (bench (. Test m xs ys)) :verbose) 对这些进行计时

【问题讨论】:

  • 如果您正在做很多此类事情,您可能应该查看core.matrix 和/或vectorz-clj,而不是在数组上手动编码数学运算。
  • 是的,现在它们确实存在

标签: arrays optimization clojure


【解决方案1】:

在 Clojure 1.3 上试试这个:

(set! *unchecked-math* true)

(defn m [^longs xs ^longs ys]
  (dotimes [i (count xs)]
    (aset ys i
      (* 3 (aget xs i)))))

【讨论】:

  • 回到我的工作机器上,上面的 Clojure 得到 2 毫秒,Java 得到 1.95 毫秒,所有东西都使用 long。所以他们是一样的。有趣的是,我在这台机器上使用 int 获得了 800us 的 Java,即使它是相同的 JVM 和 Linux 版本(尽管对于 Clojure,int 和 long 之间没有区别)。
  • (set!unchecked-math true) - 这是否意味着所有的数学运算都将在全局范围内不被选中,或者只有这个命名空间?如何控制范围?
  • 我相信它是每个命名空间的。但是您可以在您想要未经检查的匹配的代码块之后将其关闭。它只是一个编译器标志。
  • 由于它是耳罩式的,我认为使用它的最佳方法是将您的关键定义放在(binding [*unchecked-math* true]) 中。但无论如何,很高兴看到 Clojure 以这样的方式出现! Clojure 原始支持仅涵盖 long 和 double,这就是为什么它与 int 相同,我想说。
  • @Hendekagon 注意到(set! ....)defn 处于同一级别,因此它在编译和加载时执行。线程问题无关紧要。
【解决方案2】:

如果您想要速度,您需要进入原始世界,在完成之前不要离开它。从盒装的Integer i 开始,然后在每个使用站点将其转换为原语是没有用的。也许你可以让dotimes 产生整数(类型提示i 的声明),但不确定。我所知道的是一个 loop-recur 构造,带有循环变量的原始初始化器:(loop [i (int 0)] ... (recur (unchecked-inc i))。此外,在您的示例中,您有(int 3)。您需要提前let,以免在每次迭代中重复拆箱。

顺便说一句,您可以使用 (int-array (range 1000000)) 创建初始化数组,而只使用 (int-array 1000000) 创建空数组。

更新

从 Clojure 1.3 开始,由于对原语的支持得到了增强,我上面写的大部分内容不再适用。 dotimes 已经使用了原始算法,所以你只需要编写来获得完整的性能是

(dotimes [i (alength ^ints xs)] 
  (aset ^ints ys i (unchecked-multiply (aget ^ints xs i) 3)

基本上,不需要int 构造函数,而是使用unchecked-multiply

【讨论】:

  • 好的,所以我现在在另一台机器上,但到目前为止的结果:原始是 36 毫秒,(let [z (int 3)] (dotimes... 是 37 毫秒,(int (count xs ))... 是 35 毫秒,(前两个)都是 37 毫秒,删除类型提示(int i)也是 37 毫秒,循环递归形式也是 37 毫秒(并不奇怪,因为 dotimes 是通过循环递归实现的)
  • 问题不在于它是否使用loop-recur 实现(当然是),而是循环变量是否绑定到原始数​​字或盒装数字。事实证明,在 Clojure 1.3 中它是一个原始 var,所以我建议在原始循环递归中实现的内容与使用普通的 dotime 完全相同。但是,您需要使用长整数而不是整数。永远不要强迫 clojure 从 long back 强制转换为 int。
  • 我也在测试这个。通过使用 unchecked-multiply 代替 *,我获得了 25% 的优势。 RE: longs -> ints -- 你使用的是 64 位虚拟机吗?在我的盒子里,int 和 long 的表现是一样的,所以没有任何收获。
  • 为了进一步了解这一点,请尝试检查生成的字节码并与 Java 进行比较。我不知道您是否可以在没有 gen-class 的情况下进行检查,但应该可以。
  • 我使用的是 64 位 VM,是的,int 和 long 对我来说也以相同的速度运行。这个gist.github.com/2397907 对我来说是迄今为止最快的实现......更新哦! unchecked-multiply 给出 6.5ms gist.github.com/2397946
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-13
  • 2014-03-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多