【问题标题】:Fast complex number arithmetic in ClojureClojure 中的快速复数运算
【发布时间】:2012-08-03 05:16:04
【问题描述】:

我在 Clojure 中实现了一些基本的复数运算,并注意到它比大致等效的 Java 代码慢了大约 10 倍,即使有类型提示也是如此。

比较:

(defn plus [[^double x1 ^double y1] [^double x2 ^double y2]]
    [(+ x1 x2) (+ y1 y2)])

(defn times [[^double x1 ^double y1] [^double x2 ^double y2]]
    [(- (* x1 x2) (* y1 y2)) (+ (* x1 y2) (* y1 x2))])

(time (dorun (repeatedly 100000 #(plus [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(times [1 0] [0 1])))) 

输出:

"Elapsed time: 69.429796 msecs"
"Elapsed time: 72.232479 msecs"

与:

public static void main( String[] args ) {
  double[] z1 = new double[] { 1, 0 };
  double[] z2 = new double[] { 0, 1 };
  double[] z3 = null;

  long l_StartTimeMillis = System.currentTimeMillis();
  for ( int i = 0; i < 100000; i++ ) {
    z3 = plus( z1, z2 ); // assign result to dummy var to stop compiler from optimising the loop away
  }
  long l_EndTimeMillis = System.currentTimeMillis();
  long l_TimeTakenMillis = l_EndTimeMillis - l_StartTimeMillis;
  System.out.format( "Time taken: %d millis\n", l_TimeTakenMillis );


  l_StartTimeMillis = System.currentTimeMillis();
  for ( int i = 0; i < 100000; i++ ) {
    z3 = times( z1, z2 );
  }
  l_EndTimeMillis = System.currentTimeMillis();
  l_TimeTakenMillis = l_EndTimeMillis - l_StartTimeMillis;
  System.out.format( "Time taken: %d millis\n", l_TimeTakenMillis );

  doNothing( z3 );
}

private static void doNothing( double[] z ) {

}

public static double[] plus (double[] z1, double[] z2) {
  return new double[] { z1[0] + z2[0], z1[1] + z2[1] };
}

public static double[] times (double[] z1, double[] z2) {
  return new double[] { z1[0]*z2[0] - z1[1]*z2[1], z1[0]*z2[1] + z1[1]*z2[0] };
}

输出:

Time taken: 6 millis
Time taken: 6 millis

事实上,类型提示似乎没有什么区别:如果我删除它们,我会得到大致相同的结果。真正奇怪的是,如果我在没有 REPL 的情况下运行 Clojure 脚本,我会得到较慢的结果:

"Elapsed time: 137.337782 msecs"
"Elapsed time: 214.213993 msecs"

所以我的问题是:如何才能接近 Java 代码的性能?为什么在没有 REPL 的情况下运行 clojure 时,表达式的计算时间会更长?

更新 ==============

太好了,在deftypedefns 中使用带有类型提示的deftype,使用dotimes 而不是repeatedly 提供的性能与Java 版本一样好或更好。谢谢你们俩。

(deftype complex [^double real ^double imag])

(defn plus [^complex z1 ^complex z2]
  (let [x1 (double (.real z1))
        y1 (double (.imag z1))
        x2 (double (.real z2))
        y2 (double (.imag z2))]
    (complex. (+ x1 x2) (+ y1 y2))))

(defn times [^complex z1 ^complex z2]
  (let [x1 (double (.real z1))
        y1 (double (.imag z1))
        x2 (double (.real z2))
        y2 (double (.imag z2))]
    (complex. (- (* x1 x2) (* y1 y2)) (+ (* x1 y2) (* y1 x2)))))

(println "Warm up")
(time (dorun (repeatedly 100000 #(plus (complex. 1 0) (complex. 0 1)))))
(time (dorun (repeatedly 100000 #(times (complex. 1 0) (complex. 0 1)))))
(time (dorun (repeatedly 100000 #(plus (complex. 1 0) (complex. 0 1)))))
(time (dorun (repeatedly 100000 #(times (complex. 1 0) (complex. 0 1)))))
(time (dorun (repeatedly 100000 #(plus (complex. 1 0) (complex. 0 1)))))
(time (dorun (repeatedly 100000 #(times (complex. 1 0) (complex. 0 1)))))

(println "Try with dorun")
(time (dorun (repeatedly 100000 #(plus (complex. 1 0) (complex. 0 1)))))
(time (dorun (repeatedly 100000 #(times (complex. 1 0) (complex. 0 1)))))

(println "Try with dotimes")
(time (dotimes [_ 100000]
        (plus (complex. 1 0) (complex. 0 1))))

(time (dotimes [_ 100000]
        (times (complex. 1 0) (complex. 0 1))))

输出:

Warm up
"Elapsed time: 92.805664 msecs"
"Elapsed time: 164.929421 msecs"
"Elapsed time: 23.799012 msecs"
"Elapsed time: 32.841624 msecs"
"Elapsed time: 20.886101 msecs"
"Elapsed time: 18.872783 msecs"
Try with dorun
"Elapsed time: 19.238403 msecs"
"Elapsed time: 17.856938 msecs"
Try with dotimes
"Elapsed time: 5.165658 msecs"
"Elapsed time: 5.209027 msecs"

【问题讨论】:

  • 你试过设置*warn-on-reflection*看是否有任何反射潜入?
  • @DaoWen:不,我从未使用过该设置。我刚刚再次运行脚本并在其顶部使用(set! *warn-on-reflection* true),并且没有打印到标准输出的警告,这意味着没有使用反射,对吧?只是想确保我正确使用它。

标签: optimization clojure type-hinting numerical-computing


【解决方案1】:

性能缓慢的可能原因是:

  • Clojure 向量本质上是比 Java double[] 数组更重量级的数据结构。因此,您在创建和读取向量方面有相当多的额外开销。
  • 您将双精度数装箱作为函数的参数以及将它们放入向量中时。这种低级数字代码的装箱/拆箱成本相对较高。
  • 类型提示 (^double) 对您没有帮助:虽然您可以在普通 Clojure 函数上使用原始类型提示,但它们不适用于向量。

有关更多详细信息,请参阅此blog post on accelerating primitive arithmetic

如果您真的想要 Clojure 中的快速复数,您可能需要使用 deftype 来实现它们,例如:

(deftype Complex [^double real ^double imag])

然后使用这个类型定义你所有的复杂函数。这将使您能够始终使用原始算术,并且应该大致相当于编写良好的 Java 代码的性能。

【讨论】:

  • 对于像这样的简单类型,我认为 defrecorddeftype 更推荐。
  • @DaoWen - 我可能错了,但我相信你会从 deftype 获得更好的性能 - 它的开销(略)比 defrecord 少。 defrecord 实现了完整的类似地图的行为,更适合“业务对象数据”,而 deftype 更适合稍低级别的数据类型。
  • 谢谢,我想知道 deftype/defrecord 但认为它们可能会引入更多开销,但我会尝试 deftype(以及该博客文章中的内容)并报告。
  • @mikera - 关于 deftype 是较低级别的绝对是正确的,但我认为使用 defrecord 代替 deftype 不一定会引入任何额外的开销。如果您不调用任何方法,实现额外的接口(例如 IPersistentMap)不会伤害您。使用 deftype 代替 defrecord 阻止您对实例进行关键字查找和解构,这在对性能不太重要的部分代码中可能很有用。
  • ^:static 与类型提示无关,至少从 1.2 开始就没有。从 1.3 开始,您可以将原始类型提示作为函数参数;但是,OP 没有,因为他接受向量,而不是基元,并且必须将双打装箱以适应。除此之外,我同意您最终使用 deftype 的建议。
【解决方案2】:
  • 我对基准测试了解不多,但您似乎需要 开始测试时预热 jvm。因此,当您在 REPL 中执行此操作时,它已经预热了。当你作为脚本运行时,它还没有。

  • 在 java 中,您在 1 个方法中运行所有循环。除了plustimes 之外,没有其他方法被调用。在 clojure 中,您创建匿名函数并重复调用它来调用它。这需要一些时间。您可以将其替换为dotimes

我的尝试:

(println "Warm up")
(time (dorun (repeatedly 100000 #(plus [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(times [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(plus [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(times [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(plus [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(times [1 0] [0 1]))))

(println "Try with dorun")
(time (dorun (repeatedly 100000 #(plus [1 0] [0 1]))))
(time (dorun (repeatedly 100000 #(times [1 0] [0 1]))))

(println "Try with dotimes")
(time (dotimes [_ 100000]
        (plus [1 0] [0 1])))

(time (dotimes [_ 100000]
        (times [1 0] [0 1])))

结果:

Warm up
"Elapsed time: 367.569195 msecs"
"Elapsed time: 493.547628 msecs"
"Elapsed time: 116.832979 msecs"
"Elapsed time: 46.862176 msecs"
"Elapsed time: 27.805174 msecs"
"Elapsed time: 28.584179 msecs"
Try with dorun
"Elapsed time: 26.540489 msecs"
"Elapsed time: 27.64626 msecs"
Try with dotimes
"Elapsed time: 7.3792 msecs"
"Elapsed time: 5.940705 msecs"

【讨论】:

  • 谢谢,有道理。我得到了类似的结果。
猜你喜欢
  • 2010-10-31
  • 2012-08-23
  • 2011-04-18
  • 2020-06-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多