【问题标题】:Why do function calls slow things down in clojure?为什么函数调用会减慢 clojure 中的速度?
【发布时间】:2013-11-10 04:28:22
【问题描述】:

我一直在玩Is Clojure is Still Fast?(和前传Clojure is Fast)代码。不幸的是,内联微分方程 (f) 是提高性能所采取的步骤之一。在不这样做的情况下,我能想到的最干净/最快的事情如下:

; As in the referenced posts, for giving a rough measure of cycles/iteration (I know this is a very rough
; estimate...)
(def cpuspeed 3.6) ;; My computer runs at 3.6 GHz
(defmacro cyclesperit [expr its]
  `(let [start# (. System (nanoTime))
         ret# ( ~@expr (/ 1.0 ~its) ~its )
         finish# (. System (nanoTime))]
     (println (int (/ (* cpuspeed (- finish# start#)) ~its)))))

;; My solution
(defn f [^double t ^double y] (- t y))
(defn mysolveit [^double t0 ^double y0 ^double h ^long its]
  (if (> its 0)
    (let [t1 (+ t0 h)
          y1 (+ y0 (* h (f t0 y0)))]
      (recur t1 y1 h (dec its)))
    [t0 y0 h its]))
; => 50-55 cycles/it

; The fastest solution presented by the author (John Aspden) is
(defn faster-solveit [^double t0 ^double y0 ^double h ^long its]
  (if (> its 0)
    (let [t1 (+ t0 h)
          y1 (+ y0 (* h (- t0 y0)))]
      (recur t1 y1 h (dec its)))
    [t0 y0 h its]))
; => 25-30 cycles/it

我的解决方案中的类型提示很有帮助(它是 224 个周期/它在 fsolveit 上没有类型提示),但它仍然比内联版本慢近 2 倍。最终这个表现还是相当不错的,但这次命中很不幸。

为什么会有这样的性能影响?有办法解决吗?是否有计划找到改进的方法?正如 John 在原帖中指出的那样,函数调用在函数式语言中效率低下似乎很有趣/不幸。

注意:我正在运行 Clojure 1.5 并在 project.clj 文件中有:jvm-opts ^:replace [],这样我就可以使用 lein exec/run 而不会减慢速度(如果你不这样做,我发现它会。 ..)

【问题讨论】:

    标签: performance clojure


    【解决方案1】:

    在存在 JIT 编译器的情况下进行基准测试很棘手;你真的必须允许一个热身期,但是你也不能只是在一个循环中运行它,因为它可能会被证明是无操作的并被优化掉。在 Clojure 中,通常的解决方案是使用 Hugo Duncan 的Criterium

    (solveit 0.0 1.0 (/ 1.0 1000000) 1000000) 的两个版本的solveit 运行Criterium 基准测试结果在我的机器上几乎完全相同(mysolveit ~3.44 毫秒,faster-solveit ~3.45 毫秒)。这是在使用 Criterium 0.4.2 (criterium.core/bench) 以 -XX:+UseConcMarkSweepGC 运行的 64 位 JVM 中。大概 HotSpot 只是内联f。无论如何,根本不会影响性能。

    【讨论】:

    • 哇...非常感谢。我读过 clojure 中的性能分析是 JVM 的一个有点棘手的原因,但我认为差异很小。我刚刚分析了一个函数,它使用时间与 bench 有大约 20 倍的差异!
    【解决方案2】:

    除了已经很好的答案之外,JVM JIT 通常会在预热时内联原始函数调用,在这种情况下,当您使用预热的 JIT 对其进行替换时,您会看到相同的结果。只是想说,Clojure 也有一个内联功能,不过对于产生好处的情况。

    (defn f
      {:inline-arities #{2}
       :inline (fn [t y] `(- (double ~t) (double ~y)))}
      ^double [^double t ^double y]
      (- t y))
    

    现在 Clojure 将编译掉对 f 的调用,在编译时内联函数。而 JIT 将根据需要在运行时内联函数。

    还要注意,我在f 的返回值中添加了^double 类型提示,如果你不这样做,它会被编译为返回Object,并且需要添加一个演员表,我不确定这是否真的对性能有很大影响,但如果你想要一个完全原始的函数,它接受原语并返回原语,你还需要输入提示返回。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-11-30
      • 1970-01-01
      • 1970-01-01
      • 2020-05-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多