何时在 Kotlin 使用内联函数?

我知道内联函数可能会提高性能并导致生成的代码增长,但我不确定什么时候使用内联函数是正确的。

lock(l) { foo() }

编译器可以发出以下代码,而不是为参数创建函数对象并生成调用。(来源)

l.lock()
try {
foo()
}
finally {
l.unlock()
}

但我发现 Kotlin 没有为非内联函数创建函数对象,为什么?

/**non-inline function**/
fun lock(lock: Lock, block: () -> Unit) {
lock.lock();
try {
block();
} finally {
lock.unlock();
}
}
60074 次浏览

假设您创建了一个高阶函数,该函数采用 () -> Unit类型的 lambda (没有参数,没有返回值) ,并按如下方式执行:

fun nonInlined(block: () -> Unit) {
println("before")
block()
println("after")
}

用 Java 的说法,这将转化为类似这样的东西(简化!) :

public void nonInlined(Function block) {
System.out.println("before");
block.invoke();
System.out.println("after");
}

当你从 Kotlin 打来的时候..。

nonInlined {
println("do something here")
}

在引擎盖下面,将在这里创建一个 Function实例,它将代码包装在 lambda 中(同样,这是简化的) :

nonInlined(new Function() {
@Override
public void invoke() {
System.out.println("do something here");
}
});

因此,基本上,调用这个函数并向其传递一个 lambda 将始终创建一个 Function对象的实例。


另一方面,如果使用 inline关键字:

inline fun inlined(block: () -> Unit) {
println("before")
block()
println("after")
}

当你这样称呼它:

inlined {
println("do something here")
}

不会创建 Function实例,相反,内联函数中 block调用的代码将被复制到调用站点,因此您将在字节码中看到类似的内容:

System.out.println("before");
System.out.println("do something here");
System.out.println("after");

在这种情况下,不会创建新实例。

使用 inline 修饰符时,最重要的情况是使用参数函数定义类 util 函数。集合或字符串处理(如 filtermapjoinToString)或单独的函数就是一个很好的例子。

这就是为什么内联修饰符对于库开发人员来说是一个重要的优化。他们应该知道它是如何工作的,它的改进和成本是什么。当我们使用函数类型参数定义自己的 util 函数时,我们应该在项目中使用 inline 修饰符。

如果我们没有函数类型参数,实体化类型参数,并且我们不需要非本地返回,那么我们很可能不应该使用内联修饰符。这就是为什么我们会在 Android Studio 或 IDEA IntelliJ 上收到警告。

此外,还存在代码大小问题。内联一个大型函数可以显著增加字节码的大小,因为它被复制到每个调用站点。在这种情况下,可以重构函数并将代码提取到常规函数。

让我补充一点:

  1. 如果您有一个不接受其他函数作为参数的简单函数,那么内联它们是没有意义的。IntelliJ 会警告你:

    内联“ ...”的预期性能影响不大。 内联最适用于具有函数类型参数的函数

  2. 即使您有一个“带有函数类型参数”的函数,您也可能会遇到编译器告诉您内联不起作用。考虑一下这个例子:

     inline fun calculateNoInline(param: Int, operation: IntMapper): Int {
    val o = operation //compiler does not like this
    return o(param)
    }
    

    这段代码无法编译,导致错误:

    在“ ...”中非法使用内联参数“ operation”。在参数声明中添加“ noinline”修饰符。

    原因是编译器无法内联此代码,特别是 operation参数。如果 operation没有包装在一个对象中(这将是应用 inline的结果) ,那么如何将它分配给一个变量呢?在这种情况下,编译器建议使用参数 noinline。只有一个 noinline函数的 inline函数没有任何意义,不要这样做。但是,如果有多个函数类型的参数,那么如果需要,可以考虑内联其中的一些参数。

因此,这里有一些建议的规则:

  • 当所有函数类型参数被直接调用或传递给 其他内联函数时,可以将内联
  • 在这种情况下,应该是内联的。
  • 当函数参数被分配给函数内部的变量时,不能内联
  • 如果您的函数类型参数中至少有一个可以内联,那么您可以考虑内联,对于其他函数类型参数使用 noinline
  • 你的 不应该内嵌了巨大的函数,想想生成的字节码。它将被复制到调用函数的所有位置。
  • 另一个用例是 reified类型参数,它要求您使用 inline

您可能需要一个简单的例子是当您创建一个包含一个挂起块的 util 函数时。考虑一下。

fun timer(block: () -> Unit) {
// stuff
block()
//stuff
}


fun logic() { }


suspend fun asyncLogic() { }


fun main() {
timer { logic() }


// This is an error
timer { asyncLogic() }
}

在这种情况下,我们的计时器不接受挂起函数。为了解决这个问题,您可能也想让它挂起来

suspend fun timer(block: suspend () -> Unit) {
// stuff
block()
// stuff
}

但它只能从协程/挂起函数本身使用。然后,您将最终生成这些工具的异步版本和非异步版本。如果你让它内联,问题就会消失。

inline fun timer(block: () -> Unit) {
// stuff
block()
// stuff
}


fun main() {
// timer can be used from anywhere now
timer { logic() }


launch {
timer { asyncLogic() }
}
}

这是一个带有错误状态的 Kotlin 游乐场。让计时器内联来解决它。

高阶函数 非常有用,它们确实可以改进代码的 reusability。然而,使用它们最大的问题之一是效率。Lambda 表达式被编译成类(通常是匿名类) ,在 Java 中创建对象是一项繁重的操作。通过使函数内联,我们仍然可以有效地使用高阶函数,同时保持所有的好处。

内联函数内联函数进入画面

当一个函数被标记为 inline时,在代码编译期间,编译器将用该函数的实际主体替换所有的函数调用。此外,作为参数提供的 lambda 表达式被替换为它们的实际主体。它们将不作为函数处理,而作为实际代码处理。

简而言之:- Inline —— > 不是被调用,而是在编译时被函数的主体代码替换..。

在 Kotlin,使用一个函数作为另一个函数(所谓的高阶函数)的参数感觉比在 Java 中更自然。

不过,使用 lambdas 有一些缺点。因为它们是匿名类(因此也是对象) ,所以它们需要内存(甚至可能会增加应用程序的整体方法计数)。 为了避免这种情况,我们可以内联我们的方法。

fun notInlined(getString: () -> String?) = println(getString())


inline fun inlined(getString: () -> String?) = println(getString())

从上面的例子 :-这两个函数做完全相同的事情-打印 getString 函数的结果。一个是内联的,一个不是。

如果您检查反编译的 Java 代码,您将看到这些方法是完全相同的。这是因为 inline 关键字是指示编译器将代码复制到调用站点。

但是,如果我们将任何函数类型传递给另一个函数,如下所示:

//Compile time error… Illegal usage of inline function type ftOne...
inline fun Int.doSomething(y: Int, ftOne: Int.(Int) -> Int, ftTwo: (Int) -> Int) {
//passing a function type to another function
val funOne = someFunction(ftOne)
/*...*/
}

为了解决这个问题,我们可以重写函数如下:

inline fun Int.doSomething(y: Int, noinline ftOne: Int.(Int) -> Int, ftTwo: (Int) -> Int) {
//passing a function type to another function
val funOne = someFunction(ftOne)
/*...*/}

假设我们有一个如下的高阶函数:

inline fun Int.doSomething(y: Int, noinline ftOne: Int.(Int) -> Int) {
//passing a function type to another function
val funOne = someFunction(ftOne)
/*...*/}

在这里,当只有一个 lambda 参数并且我们将它传递给另一个函数时,编译器将告诉我们不要使用 inline 关键字。因此,我们可以重写以下函数:

fun Int.doSomething(y: Int, ftOne: Int.(Int) -> Int) {
//passing a function type to another function
val funOne = someFunction(ftOne)
/*...*/
}

注意 :-我们还必须删除关键字 noinline,因为它只能用于内联函数!

假设我们有这样的函数 —— >

fun intercept() {
// ...
val start = SystemClock.elapsedRealtime()
val result = doSomethingWeWantToMeasure()
val duration = SystemClock.elapsedRealtime() - start
log(duration)
// ...}

这种方法工作得很好,但是函数逻辑的主要部分被度量代码污染了,这使得您的同事更难处理正在发生的事情。 :)

下面是内联函数如何帮助这段代码:

 fun intercept() {
// ...
val result = measure { doSomethingWeWantToMeasure() }
// ...
}
}


inline fun <T> measure(action: () -> T) {
val start = SystemClock.elapsedRealtime()
val result = action()
val duration = SystemClock.elapsedRealtime() - start
log(duration)
return result
}

现在,我可以集中精力阅读拦截()函数的主要目的是什么,而不必跳过测量代码行。我们还可以选择在我们希望的其他地方重用这些代码

Inline 允许您使用闭包({ ... })中的 lambda 参数调用函数,而不是传递与 lambda 类似的度量(myLamda)

这什么时候有用?

Inline 关键字对于接受其他函数或 lambdas 作为参数的函数非常有用。

如果函数没有 inline 关键字,那么函数的 lambda 参数在编译时就会转换成一个函数接口的实例,只有一个方法叫做  () ,而 lambda 中的代码是通过在函数主体中的那個函数實例上呼叫  ()来执行的。

对于函数的 inline 关键字,编译时间转换永远不会发生。相反,内联函数的主体被插入到它的调用站点,并且它的代码被执行,而没有创建函数实例的开销。

嗯? 安卓系统中的例子—— >

假设我们在一个活动路由器类中有一个函数来启动一个活动并应用一些额外的功能

fun startActivity(context: Context,
activity: Class<*>,
applyExtras: (intent: Intent) -> Unit) {
val intent = Intent(context, activity)
applyExtras(intent)
context.startActivity(intent)
}

这个函数创建一个意图,通过调用 appyExtras 函数参数应用一些额外的参数,然后启动活动。

如果我们查看已编译的字节码并将其反编译为 Java,这看起来像是:

void startActivity(Context context,
Class activity,
Function1 applyExtras) {
Intent intent = new Intent(context, activity);
applyExtras.invoke(intent);
context.startActivity(intent);
}

假设我们从一个活动中的点击侦听器调用:

override fun onClick(v: View) {
router.startActivity(this, SomeActivity::class.java) { intent ->
intent.putExtra("key1", "value1")
intent.putExtra("key2", 5)
}
}

这个 click 侦听器的反编译字节码如下所示:

@Override void onClick(View v) {
router.startActivity(this, SomeActivity.class, new Function1() {
@Override void invoke(Intent intent) {
intent.putExtra("key1", "value1");
intent.putExtra("key2", 5);
}
}
}

每次单击侦听器被触发时,都会创建 Function1的一个新实例。这个工作很好,但它不是理想的!

现在,让我们将内联添加到活动路由器方法中:

inline fun startActivity(context: Context,
activity: Class<*>,
applyExtras: (intent: Intent) -> Unit) {
val intent = Intent(context, activity)
applyExtras(intent)
context.startActivity(intent)
}

在完全不改变我们的 click 侦听器代码的情况下,我们现在能够避免创建 Function1实例。Java 中的 click 侦听器代码现在看起来是这样的:

@Override void onClick(View v) {
Intent intent = new Intent(context, SomeActivity.class);
intent.putExtra("key1", "value1");
intent.putExtra("key2", 5);
context.startActivity(intent);
}

就是这样. . :)

“内联”函数基本上意味着复制函数体并将其粘贴到函数的调用站点。这在编译时发生。

使用 inline防止对象创建

Lambdas 被转换为类

在 Kotlin/JVM 中,函数类型(lambdas)被转换为扩展接口 Function的匿名/常规类:

fun doSomethingElse(lambda: () -> Unit) {
println("Doing something else")
lambda()
}

编译后,上面的函数如下所示:

public static final void doSomethingElse(Function0 lambda) {
System.out.println("Doing something else");
lambda.invoke();
}

函数类型 () -> Unit转换为接口 Function0

现在让我们看看从其他函数调用这个函数时会发生什么:

fun doSomething() {
println("Before lambda")
doSomethingElse {
println("Inside lambda")
}
println("After lambda")
}

问题: 物体

编译器将 lambda 替换为 Function类型的匿名对象:

public static final void doSomething() {
System.out.println("Before lambda");
doSomethingElse(new Function() {
public final void invoke() {
System.out.println("Inside lambda");
}
});
System.out.println("After lambda");
}

这里的问题是,如果在循环中数千次调用此函数,将创建数千个对象并收集垃圾。这会影响性能。

解决方案: inline

通过在函数之前添加 inline关键字,我们可以告诉编译器在调用站点复制该函数的代码 而不是创造物体:

inline fun doSomethingElse(lambda: () -> Unit) {
println("Doing something else")
lambda()
}

这导致在呼叫站复制 inline函数的代码以及 lambda()的代码:

public static final void doSomething() {
System.out.println("Before lambda");
System.out.println("Doing something else");
System.out.println("Inside lambda");
System.out.println("After lambda");
}

如果与/不与 inline关键字相比,在 for循环中有100万次重复,那么执行速度将加倍。因此,使用其他函数作为参数的函数在内联时速度更快。


使用 inline防止捕获变量

当你在 lambda 中使用局部变量时,它被称为变量捕获(闭包) :

fun doSomething() {
val greetings = "Hello"                // Local variable
doSomethingElse {
println("$greetings from lambda")  // Variable capture
}
}

如果这里的 doSomethingElse()函数不是 inline,那么捕获的变量将通过构造函数传递给 lambda,同时创建我们之前看到的匿名对象:

public static final void doSomething() {
String greetings = "Hello";
doSomethingElse(new Function(greetings) {
public final void invoke() {
System.out.println(this.$greetings + " from lambda");
}
});
}

如果在 lambda 中使用了许多本地变量,或者在循环中调用 lambda,那么通过构造函数传递每个本地变量会导致额外的内存开销。在这种情况下使用 inline函数很有帮助,因为变量是直接在调用站点上使用的。

因此,正如您从上面的两个示例中看到的,当函数使用其他函数作为参数时,就可以实现 inline函数的大块性能优势。这是 inline函数最有益和最值得使用的时候。没有必要使用 inline其他通用函数,因为 JIT 编译器已经在需要的时候将它们内联起来。


使用 inline更好地控制流程

由于非内联函数类型被转换为类,我们不能在 lambda 中写入 return语句:

fun doSomething() {
doSomethingElse {
return    // Error: return is not allowed here
}
}

这称为 非本地 return,因为它不是调用函数 doSomething()的本地函数。不允许非本地 return的原因是 return语句存在于另一个类中(在前面显示的匿名类中)。使用 doSomethingElse()函数 inline可以解决这个问题,我们可以使用非本地返回,因为这样就可以将 return语句复制到调用函数中。


对于 reified类型参数使用 inline

在 Kotlin 使用泛型时,我们可以使用类型 T的值。但是我们不能直接处理类型,我们得到了错误 Cannot use 'T' as reified type parameter. Use a class instead:

fun <T> doSomething(someValue: T) {
println("Doing something with value: $someValue")               // OK
println("Doing something with type: ${T::class.simpleName}")    // Error
}

这是因为我们传递给函数的类型参数在运行时被擦除。因此,我们不可能确切地知道我们正在处理的是哪种类型。

使用 inline函数和 reified类型参数解决了这个问题:

inline fun <reified T> doSomething(someValue: T) {
println("Doing something with value: $someValue")               // OK
println("Doing something with type: ${T::class.simpleName}")    // OK
}

内联导致将实际的类型参数复制到 T中。例如,当您调用类似于 doSomething("Some String")的函数时,T::class.simpleName变成了 String::class.simpleNamereified关键字只能与 inline函数一起使用。


当调用是重复的时候避免使用 inline

假设我们有这样一个函数,它在不同的抽象级别上被重复调用:

inline fun doSomething() {
println("Doing something")
}

抽象层

inline fun doSomethingAgain() {
doSomething()
doSomething()
}

结果:

public static final void doSomethingAgain() {
System.out.println("Doing something");
System.out.println("Doing something");
}

在第一个抽象层,代码增长为: 21 = 2行。

第二抽象层

inline fun doSomethingAgainAndAgain() {
doSomethingAgain()
doSomethingAgain()
}

结果:

public static final void doSomethingAgainAndAgain() {
System.out.println("Doing something");
System.out.println("Doing something");
System.out.println("Doing something");
System.out.println("Doing something");
}

在第二个抽象层,代码增长为: 22 = 4行。

第三抽象层

inline fun doSomethingAgainAndAgainAndAgain() {
doSomethingAgainAndAgain()
doSomethingAgainAndAgain()
}

结果:

public static final void doSomethingAgainAndAgainAndAgain() {
System.out.println("Doing something");
System.out.println("Doing something");
System.out.println("Doing something");
System.out.println("Doing something");
System.out.println("Doing something");
System.out.println("Doing something");
System.out.println("Doing something");
System.out.println("Doing something");
}

在第三个抽象层,代码增长为: 23 = 8行。

类似地,在第四个抽象层,代码以24 = 16行的速度增长,依此类推。

数字2是函数在每个抽象层被调用的次数。正如您所看到的,代码不仅在最后一个级别上呈指数级增长,而且在每一个级别上都呈指数级增长,所以是16 + 8 + 4 + 2行。为了保持简洁,我在这里只展示了2个调用和3个抽象级别,但是想象一下要为更多的调用和更多的抽象级别生成多少代码。这增加了应用程序的大小。这是另一个原因,为什么你不应该在你的应用程序的每一个功能 inline


在递归循环中避免使用 inline

避免在函数调用的递归周期中使用 inline函数,如下面的代码所示:

// Don't use inline for such recursive cycles


inline fun doFirstThing() { doSecondThing() }
inline fun doSecondThing() { doThirdThing() }
inline fun doThirdThing() { doFirstThing() }

这将导致复制代码的函数无休止地循环。编译器给出一个错误: The 'yourFunction()' invocation is a part of inline cycle


隐藏实现时不能使用 inline

公共 inline函数不能访问 private函数,因此它们不能用于实现隐藏:

inline fun doSomething() {
doItPrivately()  // Error
}


private fun doItPrivately() { }

在上面所示的 inline函数中,访问 private函数 doItPrivately()会出现一个错误: Public-API inline function cannot access non-public API fun


检查生成的代码

现在,关于你问题的第二部分:

但是我发现 Kotlin 没有创建函数对象 非内联函数,为什么?

确实创建了 Function对象。要查看创建的 Function对象,您需要在 main()函数内实际调用 lock()函数,如下所示:

fun main() {
lock { println("Inside the block()") }
}

生成类

生成的 Function类没有反映在反编译的 Java 代码中。您需要直接查看字节码。查找以下行开头的句子:

final class your/package/YourFilenameKt$main$1 extends Lambda implements Function0 { }

这是编译器为传递给 lock()函数的函数类型生成的类。main$1是为 block()函数创建的类的名称。有时类是匿名的,如第一部分中的示例所示。

生成的对象

在字节码中,查找以下行开头的代码:

GETSTATIC your/package/YourFilenameKt$main$1.INSTANCE

INSTANCE是为上面提到的类创建的对象。创建的对象是一个单例对象,因此命名为 INSTANCE


就是这样! 希望对 inline函数提供有用的见解。

fun higherOrder(lambda:():Unit){
//invoking lambda
lambda()
}


//Normal function calling higher-order without inline
fun callingHigerOrder() {
higherOrder()
//Here an object will be created for the lambda inside the higher-order function
}


//Normal function calling higher-order with inline
fun callingHigerOrder() {
higherOrder()
//Here there will be no object created and the contents of the lambda will be called directly into this calling function.
}

如果希望避免在调用端创建对象,请使用内联。 因此,当使用内联时,正如我们所理解的,lambda 将是调用函数的一部分,如果在 lambda 块中有一个返回调用,那么整个调用函数将得到返回,这被称为非本地返回。 为了避免非本地返回使用交叉内联前 lambda 块在高阶函数。