请注意,通常情况下内联提供的性能优势很小甚至没有,因为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)
(add-inline 1 2)
(reduce add-macro [1 2])
(reduce add-inline [1 2])
宏(Macros)的工作方式类似于应用于带有元标记 :macro true
的变量,该标记告诉编译器不要调用具有已评估参数的 Var 中的函数,而是使用未评估的参数调用函数,并用其返回的扩展替换整个表单。编译器将在执行 :inline
之前检查 :macro
元标记,这是一个细节,但如果你想知道的话,在找到两个元标记 :macro
和 :inline
的情况下,宏将获胜。
definline
更接近于“实验性”的概念,意味着使用时需自行承担风险,而非即将被弃用的状态。它和最初添加时一样,并没有任何旨在取代它的东西。此外,:inline
元数据和definline
之间的联系也不是很清晰:两者中的一个可能被弃用而另一个则未必。 - amalloydefinline
可以用于不必实现两者,只需提供宏即可生成其回退函数,使其函数体和内联扩展执行相同的操作,但需要确保您的内联扩展仅执行类似函数的操作。 - Didier A.