【问题标题】:How does Clojure ^:const work?Clojure ^:const 是如何工作的?
【发布时间】:2012-02-28 01:44:14
【问题描述】:

我试图了解 ^:const 在 clojure 中的作用。这就是开发文档所说的。 http://dev.clojure.org/display/doc/1.3

(定义常量 {:pi 3.14 :e 2.71})

(def ^:const pi (:pi 常数)) (def ^:const e (:e 常量))

在映射中查找 :e 和 :pi 的开销发生在编译时,因为 (:pi 常量) 和 (:e 常量) 在它们的父 def 形式被求值时被求值。

这令人困惑,因为元数据是针对绑定到符号 pi 的 var 和绑定到符号 e 的 var,但下面的句子说它有助于加快地图查找,而不是 var 查找。

有人可以解释^:const 在做什么以及使用它的理由吗?这与使用巨大的 let 块或使用像 (pi)(e) 这样的宏相比如何?

【问题讨论】:

    标签: clojure constants


    【解决方案1】:

    在示例文档中,他们试图表明,在大多数情况下,如果您 def 一个 var 是在不使用 const 的情况下在地图中查找某些内容的结果,那么查找将在类加载时发生。 所以每次运行程序时都要支付一次费用(不是在每次查找时,只是在类加载时)。并支付每次读取时在 var 中查找值的成本。

    如果您改为将其设为 const,则编译器将在编译时进行查找,然后发出一个简单的 java final 变量,您只需在编译时支付一次查找费用程序。

    这是一个人为的例子,因为在类加载时进行一次 map 查找和在运行时进行一些 var 查找基本上什么都不是,尽管它说明了一些工作发生在编译时,一些工作发生在加载时,其余的很好.. . 其余时间

    【讨论】:

    • 我认为这不准确。更重要的节省不是 map-lookup,而是 pie 的 var-lookup,如果缺少 ^:const,每次引用它们时都会发生这种情况,但根本不会发生^:const 包括在内。
    • 感谢您指出这一点,我已经编辑添加了 var 查找。
    【解决方案2】:

    这对我来说似乎是一个糟糕的例子,因为关于地图查找的东西只会混淆这个问题。

    一个更现实的例子是:

    (def pi 3.14)
    (defn circ [r] (* 2 pi r))
    

    在这种情况下,圆周的主体被编译成在运行时取消引用 pi 的代码(通过调用 Var.getRawRoot),每次调用圆周。

    (def ^:const pi 3.14)
    (defn circ2 [r] (* 2 pi r))
    

    在这种情况下,circ2 被编译成完全相同的代码,就好像它是这样编写的:

    (defn circ2 [r] (* 2 3.14 r))
    

    也就是说,跳过了对 Var.getRawRoot 的调用,这样可以节省一点时间。这是一个快速测量,其中 circ 是上面的第一个版本,而 circ2 是第二个:

    user> (time (dotimes [_ 1e5] (circ 1)))
    "Elapsed time: 16.864154 msecs"
    user> (time (dotimes [_ 1e5] (circ2 1)))
    "Elapsed time: 6.854782 msecs"
    

    【讨论】:

    • 这是否意味着以下 (def ^:const key-to-num {:one 1 :two 2}) (def sum (+ (:one key-to-num) (:two key-to-num)) 编译为 (def sum (+ 1 2)) ?
    • 不,^:const 仅适用于原始值,不适用于任意对象
    • 这对 AOT 编译和直接链接有何影响?在这种情况下,^:const 的性能优势会消失吗?
    • @JurajMartinka 不,直接链接仅用于函数调用。 AOT 不会改变任何东西。 AOT 只是意味着 const 内联是提前完成的,而不是及时完成。因此,在这两种情况下, const 的行为都是相同的,并且仍然有利于性能(略微)。
    • @noisesmith 这不是真的, const 对对象和值同样有效。分配给 const 的任何内容都会被内联。在 robin-heggelund-hansen 的示例中,它被内联为: (def sum (+ (:one {:one 1 :two 2}) (:two {:one 1 :two 2})))。所以你仍然避免 Var 查找。
    【解决方案3】:

    除了上述效率方面,还有一个安全方面也很有用。考虑以下代码:

    (def two 2)
    (defn times2 [x] (* two x))
    (assert (= 4 (times2 2)))    ; Expected result
    
    (def two 3)                  ; Ooops! The value of the "constant" changed
    (assert (= 6 (times2 2)))    ; Used the new (incorrect) value
    
    (def ^:const const-two 2)
    (defn times2 [x] (* const-two x))
    (assert (= 4 (times2 2)))    ; Still works
    
    (def const-two 3)            ; No effect!
    (assert (= 3 const-two ))    ; It did change...
    (assert (= 4 (times2 2)))    ; ...but the function did not.
    

    因此,通过在定义变量时使用 ^:const 元数据,变量被有效地“内联”到每个使用它们的地方。因此,对 var 的任何后续更改都不会影响已内联“旧”值的任何代码。

    ^:const 的使用还具有文档功能。当阅读 (def ^:const pi 3.14159) 时,它会告诉读者 var pi 永远不会改变,它只是值 3.14159 的一个方便(并且希望是描述性的)名称。


    说了以上所有,请注意我从不在我的代码中使用^:const,因为它具有欺骗性并提供“虚假保证”,即 var 永远不会改变。问题是^:const 意味着不能重新定义变量,但正如我们在const-two 中看到的那样,它不会阻止变量被更改。相反,^:const 隐藏了 var 具有新值的事实,因为在更改 var 之前(在运行时)const-two 已被复制/内联(在编译时)到每个使用位置。

    更好的解决方案是在尝试更改 ^:const 变量时抛出异常。

    【讨论】:

    • The Clojure Style Guide 说“不要对常量使用特殊符号;除非另有说明,否则一切都假定为常量。”这是否表明值通常是内联的,至少当它们没有被重新defed 时? (也许^:const 仅表示“即使此变量有以后的def 也可能内联”,但如果没有以后的def,即使非^:consted 变量也可能内联?我的非常疯狂的猜测。)或者如果^:consted 变量不同,为什么我不应该通过命名来明确说明,例如+name+ 在 Common Lisp 中。
    • 使用 ^:const 有两种效果。 (1) 是编译器将内联该值,并且不会使用 var,因此您可以免受更改或滥用 var 的影响。 (2) 只是文档;作者声称该值不应该永远不会改变。
    • @Mars 我相信样式指南意味着 Vars 应该被假定为常量,因为在 Clojure 中,不鼓励在设置 Var 后更改其根值。变化的 Var 通常是动态 Var,并被耳罩包围。所以name 将被假定为常数(因为它永远不会改变,而不是在编译器优化或保证中),而*name* 将被假定为非常数。而^:const 只是向编译器表明他应该通过内联使用 var 来优化代码。
    猜你喜欢
    • 2013-03-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-03-11
    相关资源
    最近更新 更多