Kotlin的内联函数

内联函数/inline

简单来说,就是把函数的代码直接复制到调用处,而不是通过压栈的方式直接调用函数。

复制代码
inline fun testInline(s:String):String{
    println("内联函数")
    if(s.equals("A")){
        println("是A")
    }else{
        println("不是A")
    }
    return "Hello,$s"
}

fun testNoInline(s:String):String{
    println("非内联函数")
    if(s.equals("A")){
        println("是A")
    }else{
        println("不是A")
    }
    return "Hello,$s"
}

fun main() {
    testNoInline("World")
    testInline("World")
}

***********    **********    ****************

//编译后的代码是这样的
public final class KotlinShowKt {
    @NotNull
    public static final String testInline(@NotNull String s) {
        Intrinsics.checkNotNullParameter(s, "s");
        System.out.println((Object) "内联函数");
        if (s.equals("A")) {
            System.out.println((Object) "是A");
        } else {
            System.out.println((Object) "不是A");
        }
        return "Hello," + s;
    }

    @NotNull
    public static final String testNoInline(@NotNull String s) {
        Intrinsics.checkNotNullParameter(s, "s");
        System.out.println((Object) "非内联函数");
        if (s.equals("A")) {
            System.out.println((Object) "是A");
        } else {
            System.out.println((Object) "不是A");
        }
        return "Hello," + s;
    }

    public static final void main() {
        testNoInline("World");
        System.out.println((Object) "内联函数");
        if ("World".equals("A")) {
            System.out.println((Object) "是A");
        } else {
            System.out.println((Object) "不是A");
        }
        String str = "Hello,World";
    }
}

可见,对于非inline函数,在调用的时候是直接调用函数,而调用inline函数则是直接把函数的实现代码复制到调用处。这样做有什么好处呢?

(1)直接调用方法会创建栈帧、压栈、出栈等开销,而代码复制过去,就没有这些开销,性能可以更好一点。尤其对于一些高阶函数,它们有些参数是lambda形式的,如果传给它们的不是inline函数,其执行机制将按照Java的套路来------创建匿名对象,如果是inline函数,则可避免这些,性能开销会更小。

复制代码
inline fun repeatInline(times: Int, action: (Int) -> Unit) {
    for (i in 0 until times) {
        action(i)
    }
}
fun repeatNoInline(times: Int, action: (Int) -> Unit) {
    for (i in 0 until times) {
        action(i)
    }
}


fun main() {
    repeatInline(3) { i ->
        println("i = $i")
    }
    repeatNoInline(3) { i ->
        println("i = $i")
    }
}

*************
//编译后得到的Java代码
public static final void repeatInline(int times, @NotNull Function1<? super Integer, Unit> function1) {
        Intrinsics.checkNotNullParameter(function1, "action");
        for (int i = 0; i < times; i++) {
            function1.invoke(Integer.valueOf(i));
        }
    }

    public static final void repeatNoInline(int times, @NotNull Function1<? super Integer, Unit> function1) {
        Intrinsics.checkNotNullParameter(function1, "action");
        for (int i = 0; i < times; i++) {
            function1.invoke(Integer.valueOf(i));
        }
    }

    public static final void main() {
        for (int i$iv = 0; i$iv < 3; i$iv++) {
            int i = i$iv;
            System.out.println((Object) ("i = " + i));
        }
        repeatNoInline(3, (v0) -> {
            return main$lambda$1(v0);
        });
    }

    private static final Unit main$lambda$1(int i) {
        System.out.println((Object) ("i = " + i));
        return Unit.INSTANCE;
    }

(2)允许在 lambda 中使用非局部返回(return 从外层函数返回)。

在 Kotlin 中,非局部返回 指的是:从一个 lambda 表达式内部使用 return 关键字,直接从包含这个 lambda 的外层函数返回,而不是仅仅从 lambda 本身退出。这是一种强大的控制流能力,让 lambda 可以像语言内置的关键字(如 ifforwhile)一样影响外层函数的执行流程。这一点很好理解,由于内联函数是把代码复制过去,那么内联函数里的return其实也就是相当于在调用内联函数的地方直接return了。

深度思考

有没有注意到,内联函数和非内联函数在转成Java代码后是一样的,比如上面的repeatInline和repeatNoInline。这是怎么回事

(1)向后兼容。如果Kotlin最初发布时一个函数是普通函数,后来感觉该函数性能敏感,需要改成内联函数,此时如果编译器不为其生成如上的普通Java方法,那么旧的代码想要调用的方法就没有了。也就是,该函数原来非内联函数,编译后生成了普通的Java方法,如果改成了内联函数后就不再为其生成普通的Java方法了,那旧代码就无法运行了。

(2)Java互操作性。Java是没有内联这种机制的,不会把代码复制过去,所以为了能让Java调用Kotlin的内联函数,还是要为Java生成一个普通的非内联函数。

(3)一些场景无法使用内联。比如反射调用方法,将函数作为值传递这种需要函数引用的场合等。所以也要为这些场合留下备用方案。

所以,非内联函数,还是老样子,编译成普通Java代码;内联函数,也要编译成一个普通Java方法;真正让内联函数发挥作用的是Kotlin编译器在编译Kotlin代码时做了代码复制的操作。

理论上非内联函数的lambda参数,不应该是生成一个匿名对象吗?怎么上面的repeatNoInline方法并没有生成匿名对象,而是生成了一个方法mainlambda1(int i)?

Kotlin 编译器在不同 JVM 目标版本下的优化策略不同

在JVM 1.8版本之前,确实会生成如下所示的匿名对象,编译后代码应该是这样的

... ...

repeatNoInline(3, new Function1<Integer, Unit>() {

public Unit invoke(Integer i) {

System.out.println("i = " + i);

return Unit.INSTANCE;

}

});

每次调用repeaNoInline都会生成一个匿名对象,开销比较大,而且还会生成对应类的.class文件。什么意思呢?

复制代码
fun main() {
    repeatNoInline(3) { println("first") }
    repeatNoInline(3) { println("second") }
}

如上代码,将会产生TestKt.class、TestKtmain1.class(对应第一个 lambda)、TestKtmain2.class(对应第二个 lambda)。所以开销还是比较大的。

JVM 1.8+开始,采用更高效的 Java 8 lambda 表示 + invokedynamic方案。当Kotlin编译器遇到lambda时,编译器生成的是 invokedynamic 指令,而不是 new 匿名内部类的字节码。而上述代码是我用jadx-gui查看的编译出的.class文件,反编译器在遇到**invokedynamic** 时,反编译器将 invokedynamic 翻译成了 Java 8 的 lambda 语法。实际上,还是产生了匿名对象,只是相比1.8之前,现在的方案更加高效了。invokedynamic,顾名思义,动态调用,简单来说,就是运行时根据场景做调整,也就有了优化的空间。对于无捕获的 lambda(不引用外部变量),通常使用LambdaMetafactory 生成一个单例对象(静态字段),所有调用点共享同一个实例。 即使 lambda 表达式出现在循环中,也不会反复创建新对象,只会复用那个单例。对于有捕获的 lambda(例如引用了外部局部变量 val x = 1; { println(it + x) }),每次执行到该 lambda 表达式时,仍然会创建一个新对象(因为需要保存不同的捕获值),但实现方式比匿名内部类更高效:类在运行时由 LambdaMetafactory 动态生成,不产生磁盘文件,且只生成一次 (同一个 lambda 位置,相同的捕获类型)。类加载通过内部机制完成,比从磁盘加载 .class 快得多,而且缓存在内存里。

总结:不管是1.8之前还是1.8之后,反正Java对于lambda的支持都少不了匿名对象,区别只是对lambda的支持越来越高效。

noinline

如果不想让某个函数类型的形参内联,就可以用noinline修饰

复制代码
inline fun test(a:Int,b:Int,add:(Int,Int)->Int,noinline mul:(Int,Int)->Int){
    val x = add(a,b)
    val y = mul(a,b)
    println(x)
    println(y)
}

fun main() {
    test(4,5,{x,y->x+y},{x,y->x*y})
}

//编译后
public final class KotlinShowKt {
    public static final void test(int a, int b, @NotNull Function2<? super Integer, ? super Integer, Integer> function2, @NotNull Function2<? super Integer, ? super Integer, Integer> function22) {
        Intrinsics.checkNotNullParameter(function2, "add");
        Intrinsics.checkNotNullParameter(function22, "mul");
        int x = ((Number) function2.invoke(Integer.valueOf(a), Integer.valueOf(b))).intValue();
        int y = ((Number) function22.invoke(Integer.valueOf(a), Integer.valueOf(b))).intValue();
        System.out.println(x);
        System.out.println(y);
    }

    public static final void main() {
        Function2 mul$iv = (v0, v1) -> {
            return main$lambda$1(v0, v1);
        };
        int x$iv = 4 + 5;
        int y$iv = ((Number) mul$iv.invoke(4, 5)).intValue();
        System.out.println(x$iv);
        System.out.println(y$iv);
    }

    private static final int main$lambda$1(int x, int y) {
        return x * y;
    }
}

这样,内联的函数形参就会直接代码复制形式执行,非内联的函数形参就以对象的方法的方式执行。

内联函数适合什么场景下使用?

(1)性能敏感的高频调用高阶函数。内联函数可避免匿名对象的创建,效率更高。

(2)需要非局部返回的控制流 DSL。

(3)需要泛型实化(reified)的场景。

(4)极小的工具函数(单行或几行代码)

不适合的场景

(1)函数体很大。每个调用点都会复制大片代码,导致字节码膨胀,甚至降低 CPU 缓存命中率。

(2)递归函数:编译器禁止内联递归函数。

(3)跨模块公开 API:内联函数的修改会强制所有调用方重新编译,否则二进制不兼容。

相关推荐
码农阿豪3 小时前
Python 操作金仓数据库的完全指南(上篇):连接管理与高可用
开发语言·数据库·python
计算机学姐3 小时前
基于微信小程序的校园失物招领管理系统【uniapp+springboot+vue】
java·vue.js·spring boot·mysql·信息可视化·微信小程序·uni-app
xyq20244 小时前
CSS Backgrounds(背景)
开发语言
Aurorar0rua4 小时前
CS50 x 2024 Notes C - 06
开发语言·学习方法
xyq20244 小时前
SQLite Like 子句详解
开发语言
Highcharts.js4 小时前
线形比赛积分增长或竞赛图|Highcharts企业图表代码示列
开发语言·前端·javascript·折线图·highcharts·竞赛图
古城小栈4 小时前
rust 亿级并发模型,实践完成
开发语言·网络·rust
Codigger官方4 小时前
Phoenix 语言起步指南:开启 Polyglot Singularity 之门
开发语言·人工智能·程序人生
让学习成为一种生活方式4 小时前
大肠杆菌合成扑热息痛--对乙酰氨基酚--文献精读227
开发语言·前端·javascript