Clojure函数中的: inline关键字是什么?

11

我在Clojure源代码中发现了这段代码片段:

(defn ==
  "Returns non-nil if nums all have the equivalent
  value (type-independent), otherwise false"
  {:inline (fn [x y] `(. clojure.lang.Numbers (equiv ~x ~y)))
   :inline-arities #{2}
   :added "1.0"}
   .....

:inline 是什么作用?

更新: 在这里也找到了一个很好的例子here

2个回答

11

在Clojure函数中,内联定义可使编译器将运算符视为宏而不是函数。问题在于,您必须提供一个独立的主体,它与函数主体没有任何共同之处。您可以定义一个可内联版本的+,实际上执行了-

这非常可疑且文档很少。似乎用于为小型核心函数提供更快的代码。


相比之下,在C++中,inline是提示编译器考虑对函数的任何调用进行内联扩展的一种方式。 内联扩展要具有与普通函数调用相同的语义。访问器和修改器函数经常被内联。

确实很奇怪,在Clojure语言中找到了比相应的C++更模糊的语义方面。


我曾认为该功能已弃用。它并没有。类似的definline运算符 - 不是弃用的 - 而是实验性的。要弃用,必须首先建立它,而它从未达到过那个阶段。


感谢Alan Malloy的评论做出以上更正。


它被弃用了?那在哪里写着? - amalloy
@amalloy 在这里,引用自这里 - Thumbnail
1
我并不认为这算是问题。如后续讨论所述,definline 更接近于“实验性”的概念,意味着使用时需自行承担风险,而非即将被弃用的状态。它和最初添加时一样,并没有任何旨在取代它的东西。此外,:inline 元数据和 definline 之间的联系也不是很清晰:两者中的一个可能被弃用而另一个则未必。 - amalloy
@amalloy 已经更正。 - Thumbnail
我不确定inline的语义有什么可疑的地方,这就像说函数的重载度可能会表现得非常不同,例如(+ 1 2 3)可能会执行减法而不是加法,它确实可能会这样,但从未成为问题,因为没有人会这样实现。归根结底,编写函数的人负责他们想要的语义,为什么他们想要inline版本不同取决于他们自己,而且他们可能不想要,所以他们会使它相同。 - Didier A.
此外,definline 可以用于不必实现两者,只需提供宏即可生成其回退函数,使其函数体和内联扩展执行相同的操作,但需要确保您的内联扩展仅执行类似函数的操作。 - Didier A.

3
请注意,通常情况下内联提供的性能优势很小甚至没有,因为JVM JIT能够在运行时内联热函数调用。一个预热好的JIT通常会产生与显式内联函数相同的性能。
因此,Clojure中显式内联的好处非常有限,我甚至不清楚何时会发生内联,但可以说当JIT是冷启动时,它可能会节省您一小部分时间,也就是前几次执行时。
内联的另一个好处是,它可以绕过普通函数调用中的Clojure装箱。如果您在使用interop的函数中,Java方法具有特定原始类型的重载时,当将其包装在Clojure函数中时,Clojure将在调用站点将事物转换回到包装数字,并且您的Java方法最终将被调用成为Boxed variant。但是,如果您内联它,它将跳过该函数,因此不会发生任何装箱操作,值将按照当前类型提供给Java方法。
内联的工作原理如下:
当编译器看到对变量的调用时,如果变量有定义:inline元数据,则编译器将调用:inline函数,而不是调用变量体中的函数,其中它将未评估的剩余表单传递给它,并且它永远不会调用变量的函数,而是将整个表单替换为:inline函数返回的扩展。
如果您只想内联某些参数集,但并非所有参数集,则还可以在Var上选择性地提供:inline-arities元数据,这是一个谓词,将使用当前调用的参数数目进行调用,如果它返回true,则使用:inline函数扩展它,否则将像通常一样调用Var内部的函数。
请注意,这仅适用于Var级别,不能添加:inline元数据到fn中并期望进行内联,只有在您拥有的变量时才有效。
内联非常类似于宏,实际上,您也可以使用宏来内联,唯一的区别是宏没有回退功能可以在无法使用它们的地方使用,例如它们无法在仅支持函数的地方使用,特别是在高阶调用中。例如,您无法将宏作为缩减函数传递给reduce。这就是内联的作用,可以内联的函数既具有可内联调用的宏,又具有无法使用宏的情况下的函数,因此它们起到了宏/函数混合体的作用,在可以使用宏的地方使用宏,在无法使用宏的地方使用函数。
因此,即使允许这样做,内联的函数在可以“作为宏”运行时会以不同的方式运行,这有点奇怪。
这就是为什么当你内联一个函数时,请确保它的扩展方式与函数行为相同,并且类似于无法内联时的回退函数。
还有一个“definline”宏,它定义了一个内联函数,其中它会自动从提供的内联函数中推断出回退函数,这样可以方便地不必编写两者。我认为它是“实验性的”,因为我认为不清楚它是否总能成功推断出你自己编写的回退函数。而且因为我认为他们不知道它有多有用,而且它不像使用元数据那样灵活,因为您无法控制特定的参数数量。尽管如此,如果您想要,它仍然可用。
下面是一个例子,很好地解释了内联和宏之间的区别:
(defmacro add-macro
  [a b]
  `(+ ~a ~b))

(definline add-inline
  [a b]
  `(+ ~a ~b))

(add-macro 1 2)
;; => 3

(add-inline 1 2)
;; => 3

(reduce add-macro [1 2])
;; Syntax error compiling at (src/playground.clj:62:1).
;; Can't take value of a macro: #'playground/add-macro

(reduce add-inline [1 2])
;; => 3

宏(Macros)的工作方式类似于应用于带有元标记 :macro true 的变量,该标记告诉编译器不要调用具有已评估参数的 Var 中的函数,而是使用未评估的参数调用函数,并用其返回的扩展替换整个表单。编译器将在执行 :inline 之前检查 :macro 元标记,这是一个细节,但如果你想知道的话,在找到两个元标记 :macro:inline 的情况下,宏将获胜。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接