【问题标题】:Strange aget optimisation behavior奇怪的 get 优化行为
【发布时间】:2012-04-13 16:45:13
【问题描述】:

跟进这个关于aget performance的问题

在优化方面似乎发生了一些非常奇怪的事情。我们知道以下情况是正确的:

=> (def xa (int-array (range 100000)))
#'user/xa

=> (set! *warn-on-reflection* true)
true

=> (time (reduce + (for [x xa] (aget ^ints xa x))))
"Elapsed time: 42.80174 msecs"
4999950000

=> (time (reduce + (for [x xa] (aget xa x))))
"Elapsed time: 2067.673859 msecs"
4999950000
Reflection warning, NO_SOURCE_PATH:1 - call to aget can't be resolved.
Reflection warning, NO_SOURCE_PATH:1 - call to aget can't be resolved.

然而,一些进一步的实验真的让我很奇怪:

=> (for [f [get nth aget]] (time (reduce + (for [x xa] (f xa x)))))
("Elapsed time: 71.898128 msecs"
"Elapsed time: 62.080851 msecs"
"Elapsed time: 46.721892 msecs"
4999950000 4999950000 4999950000)

没有反射警告,不需要提示。通过将 get 绑定到根 var 或 let 可以看到相同的行为。

=> (let [f aget] (time (reduce + (for [x xa] (f xa x)))))
"Elapsed time: 43.912129 msecs"
4999950000

知道为什么绑定的 get 似乎“知道”如何优化,而核心功能却不知道吗?

【问题讨论】:

    标签: performance clojure clojure-java-interop


    【解决方案1】:

    它与aget 上的:inline 指令有关,该指令扩展为(. clojure.lang.RT (aget ~a (int ~i)),而正常的函数调用涉及Reflector。试试这些:

    user> (time (reduce + (map #(clojure.lang.Reflector/prepRet 
           (.getComponentType (class xa)) (. java.lang.reflect.Array (get xa %))) xa)))
    "Elapsed time: 63.484 msecs"
    4999950000
    user> (time (reduce + (map #(. clojure.lang.RT (aget xa (int %))) xa)))
    Reflection warning, NO_SOURCE_FILE:1 - call to aget can't be resolved.
    "Elapsed time: 2390.977 msecs"
    4999950000
    

    那么,您可能想知道内联的意义何在。好吧,看看这些结果:

    user> (def xa (int-array (range 1000000))) ;; going to one million elements
    #'user/xa
    user> (let [f aget] (time (dotimes [n 1000000] (f xa n))))
    "Elapsed time: 187.219 msecs"
    user> (time (dotimes [n 1000000] (aget ^ints xa n)))
    "Elapsed time: 8.562 msecs"
    

    事实证明,在您的示例中,一旦您通过反射警告,您的新瓶颈就是 reduce + 部分而不是数组访问。此示例消除了这一点,并显示了类型提示、内联 aget 的数量级优势。

    【讨论】:

      【解决方案2】:

      当您通过高阶函数调用时,所有参数都被强制转换为对象。在这些情况下,编译器无法确定被调用函数的类型,因为在编译函数时它是未绑定的。只能确定它将是可以用一些参数调用的东西。不会打印任何警告,因为一切都会奏效。

      user> (map aget (repeat xa) (range 100))
      (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99)
      

      您已经找到了 clojure 编译器放弃并只使用对象的优势。 (这是一个过于简单的解释)

      如果你将它包装在任何自己编译的东西中(比如匿名函数),那么警告就会再次可见,尽管它们来自编译匿名函数,而不是编译对 map 的调用。

      user> (map #(aget %1 %2) (repeat xa) (range 100))
      Reflection warning, NO_SOURCE_FILE:1 - call to aget can't be resolved.
      (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99)
      

      然后当类型提示添加到匿名但不变的函数调用时,警告就会消失。

      【讨论】:

      • 我不明白的是,如果 clojure 编译器对所有事物都使用对象,为什么性能几乎与类型提示版本相同,而不是与没有类型提示的最坏情况性能版本相同给定?
      • JVM 中的 HotSpot 编译器对对象做了奇怪的(对我来说)和奇妙的事情......最坏的情况是不得不反思以在它的类中按名称查找每个函数,至少你避免那:)
      • 我只是在谈论调用作为参数传递的函数。在被调用的函数在编译时固定的情况下(“正常”情况),优化图片会更好
      猜你喜欢
      • 2010-11-16
      • 2012-08-17
      • 2017-11-25
      • 1970-01-01
      • 2019-01-01
      • 2021-02-17
      • 2020-05-07
      • 1970-01-01
      • 2023-03-14
      相关资源
      最近更新 更多