当前位置: 首页 > 知识库问答 >
问题:

何时在静态编程语言中使用内联函数?

陈开宇
2023-03-14

我知道内联函数可能会提高性能

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();
    }
}

共有3个答案

穆城
2023-03-14

lambda转换为类

在Kotlin/JVM中,函数类型(lambda)被转换为匿名/常规类,这些类扩展了接口函数。考虑以下功能:

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

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

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

函数类型()-

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

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 fun doSomethingElse(lambda: () -> Unit) {
    println("Doing something else")
    lambda()
}

这会导致在调用站点复制内联函数的代码以及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");
}

如果在for循环中使用/不使用inline关键字,重复次数达到一百万次,则执行速度会加倍。因此,将其他函数作为参数的函数在内联时速度更快。

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

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

如果此处的函数不是内联函数,则在创建前面看到的匿名对象时,捕获的变量将通过构造函数传递给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函数很有帮助,因为该变量直接用于调用站点。

因此,正如您从上面的两个示例中看到的,当函数将其他函数作为参数时,内联函数的大部分性能优势就实现了。此时,内联函数最为有用,也最值得使用。不需要内联其他通用函数,因为JIT编译器已经在必要时将它们内联。

由于非内联函数类型被转换为类,因此我们无法在lambda中编写return语句:

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

这被称为非本地返回,因为它不是调用函数do万物()的本地。不允许非本地返回的原因是返回语句存在于另一个类中(在前面显示的匿名类中)。使doThingElse()函数inline解决了这个问题,我们被允许使用非本地返回,因为然后返回语句被复制到调用函数中。

在静态编程语言中使用泛型时,我们可以使用T类型的值。但是我们不能直接使用类型,我们得到错误不能使用'T'作为具体化类型参数。改用类

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

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

使用内联函数和具体化类型参数可以解决此问题:

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

内联导致复制实际的类型参数来代替T。例如,T::类。simpleName变为String::class。simpleName,当您调用像doSomething(“Some String”)这样的函数时。具体化关键字只能与内联函数一起使用。

假设我们有以下在不同抽象级别重复调用的函数:

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行。

类似地,在第四个抽象级别,代码增长为2行4行,以此类推。

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

避免使用内联函数进行函数调用的递归循环,如以下代码所示:

// Don't use inline for such recursive cycles

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

这将导致函数无休止地复制代码。编译器会给您一个错误:调用“yourFunction()”是内联循环的一部分。

公共内联函数无法访问私有函数,因此不能用于实现隐藏:

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

private fun doItPrivately() { }

在上面显示的内联函数中,访问私有函数doitprivaty()会出现错误:公共API内联函数无法访问非公共API fun。

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

但是我发现没有kotlin为一个非内联函数创建的函数对象。为什么?

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

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

生成的类

生成的函数类不会反映在反编译的Java代码中。您需要直接查看字节码。查找以以下开头的行:

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

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

生成的对象

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

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

实例是为上述类创建的对象。创建的对象是单例对象,因此命名为实例。

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

万俟穆冉
2023-03-14

让我补充一下:当不使用内联时:

>

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

    内联“…”的预期性能影响是无关紧要的。内联最适用于具有函数类型参数的函数

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

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

    此代码无法编译,产生错误:

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

    原因是编译器无法内联此代码,尤其是操作参数。如果操作未包装在对象中(这将是应用内联操作的结果),如何将其分配给变量?在这种情况下,编译器建议将参数设置为noinline。将一个内联函数与一个非内联函数结合使用没有任何意义,请不要这样做。但是,如果有多个函数类型的参数,请考虑在需要时内联其中一些参数。

    以下是一些建议规则:

    • 当所有函数类型参数直接调用或传递给其他内联函数时,可以内联
    • 您应该在以下时间内联↑ 情况就是这样
    • 当函数参数被指定给函数内部的变量时,不能内联
    • 您应该考虑内联如果至少有一个函数类型参数可以内联,请对其他参数使用noinline
    • 您不应该内联大型函数,而应该考虑生成的字节码。它将复制到调用函数的所有位置
    • 另一个用例是具体化的类型参数,它要求您使用内联参数。请阅读此处

  • 尹臻
    2023-03-14

    假设您创建一个高阶函数,它采用()类型的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");
    }
    

    当你从科特林打电话过来时。。。

    nonInlined {
        println("do something here")
    }
    

    在引擎盖下,此处将创建一个函数的实例,该实例将代码包装在lambda中(再次简化):

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

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

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

    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");
    

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

     类似资料:
    • 我注意到Kotlin为var创建了setter,并通过setter设置值,而不是直接设置值。我们可以让setter内联吗?或者在默认情况下直接设置值而不创建私有setter方法?

    • 这个例子来自我正在学习的一门Kotlin课程: 如果我喜欢使用这样的主构造函数: 在这种情况下,我必须如何编写getter/setter?

    • 我试图用OkHttp和Cucumber在静态编程语言中设置一个Spring启动项目,并且在运行Cucumber任务时遇到以下错误。如何修复? 还有build gradle kts片段 我看到了这个错误https://github.com/square/okio/issues/647看起来可能是它,并修复了这个build.gradle,我如何将其翻译为kotlinbuild.gradle.kts?

    • 我试图转换一些使用Jackson的@JsonSubTypes注释来管理多态性的Java代码。 以下是可用的Java代码: 以下是我认为等效的Kotlin代码: 但我在三行“JsonSubTypes.Type”中的每一行都会出现以下错误: 知道吗?

    • 我希望函数位于类中(不污染全局名称空间),但可以静态访问(从不创建它们所在的对象)。提议的解决办法: 这是一个好的解决方案,还是不可避免地会创建一个对象?我应该使用哪种图案?

    • 我想为我的游戏创建一个简单的倒计时,当游戏开始时,我想每秒调用这个函数: 我试过这个: 但应用程序不幸停止,第二次调用run函数 3周前,我刚刚开始使用android开发和静态编程语言,到目前为止,我对它了解最多。 在Xcode中使用swift时,我使用了这一行,我认为类似的东西也适用于Kotlin