Kotlin - Inline Functions 1

Inline Basics

Inline or Inlining,我们更经常听到的词是方法内联或者内联函数。在大多数情况下,他们指的都是同一个意思。即,在编译期间对函数进行优化,以便让代码在机器执行时获得更高的效率。
方法内联一般可能出现在两个阶段:

  • 编译器:编译输出.class文件时
  • JVM:编译输入机器执行码时

在Java领域,方法内联是商用JVM登峰造极的虚拟机优化中重要的一环,下面节选自HotSpot文档中Method Inlining的一部分:

Inlining has important benefits. It dramatically reduces the dynamic frequency of method invocations, which saves the time needed to perform those method invocations. But even more importantly, inlining produces much larger blocks of code for the optimizer to work on. This creates a situation that significantly increases the effectiveness of traditional compiler optimizations, overcoming a major obstacle to increased Java programming language performance.

Inlining is synergistic with other code optimizations, because it makes them more effective. As the Java HotSpot compiler matures, the ability to operate on large, inlined blocks of code will open the door to a host of even more advanced optimizations in the future.

这里说JVM的方法内联带来两个优点:

  • 可以动态的减少方法调用来提高执行效率
  • 可以极大地提高其他优化手段的优化效果

但其实文档应该还想表达另外一件事情:方法内联的优化效果很难在JVM外部,更不用说单独观测到
HotSpot的方法内联是(晚期)JVM运行期进行方法内联的代表作之一,而Kotlin Inline Functions则是典型的在(早期)编译期进行方法内联。

Inline Function

首先,Kotlin的内联方法优化针对的是lambda表达式(如果不是针对lambda表达式使用IDE亦提示)。
我们知道,在Kotlin中,Function是"一等公民",每一个函数都是一个对象,并且拥有其对应的闭包。也即是说:

1. 对于每一个lambda函数都需要分配一定的内存来创建和维护Function对象。
2. 在调用lambda函数的时候,需要先访问到闭包对象,再访问到真正的函数执行块。

在多数情况下这些东西对性能的影响可以忽略不计,但是在对性能有要求时,这便是我们可以优化提升的地方。而Kotlin Inline Function 可以轻松地消除这些东西对于性能的损耗,而实现的方式类似于JVM运行期的Method Inlining(方法内联),这也是为什么Kotlin将这一手段称之为"Inline Function"

通过查看编译生成的.class文件字节码,可以清楚地看到Inline Function做的事情:

考虑这样一个例子,我们需要确保在 c() 中先执行了D类的实例方法 open() ,如果执行成功了再执行一块函数块(lambda),里面执行 foo1() foo2()。对于这个lambda函数块的执行使用 inline 来优化。
写成.kt (Kotlin Source File)是这个样子:

class C {
    fun c() {
        open(D1()) {
            foo1()
            foo2()
        }
    }
}

fun foo1(): Int {
    return 1
}

fun foo2(): Int {
    return 2
}

inline fun <T> open(d: D, body: () -> T) {
    if (d.open()) {
        body()
    }
}

而反编译出来 c() 的字节码是这样:

public final void c();
    Code:
       0: new           #8                  // class com/maxtropy/viewtest/D1
       3: dup
       4: invokespecial #11                 // Method com/maxtropy/viewtest/D1."<init>":()V
       7: checkcast     #13                 // class com/maxtropy/viewtest/D
      10: astore_1
      11: aload_1
      12: invokevirtual #17                 // Method com/maxtropy/viewtest/D.open:()Z
      15: ifeq          27
      18: nop
      19: invokestatic  #23                 // Method com/maxtropy/viewtest/CKt.foo1:()I
      22: pop
      23: invokestatic  #26                 // Method com/maxtropy/viewtest/CKt.foo2:()I
      26: pop
      27: nop
      28: return

字节码非常清楚地展示了 inline 所作的优化效果

1. Inline Function 中的内容都完全被嵌入到了 c() 当中,这意味着减少了从 c() 到真正执行方法 foo1() foo2() 之间的的函数调用
2. lambda表达式对应的函数对象也完全消失,消除了因为保存lambda表达式函数对象而造成的内存损耗。

如果我们对比以下直接使用lambda表达式而不进行inline优化 会是什么样一种情况(仅将inline关键字拿掉其他不变):

public final void c();
    Code:
       0: new           #8                  // class com/maxtropy/viewtest/D1
       3: dup
       4: invokespecial #11                 // Method com/maxtropy/viewtest/D1."<init>":()V
       7: checkcast     #13                 // class com/maxtropy/viewtest/D
      10: getstatic     #19                 // Field com/maxtropy/viewtest/C$c$1.INSTANCE:Lcom/maxtropy/viewtest/C$c$1;
      13: checkcast     #21                 // class kotlin/jvm/functions/Function0
      16: invokestatic  #27                 // Method com/maxtropy/viewtest/CKt.open:(Lcom/maxtropy/viewtest/D;Lkotlin/jvm/functions/Function0;)V
      19: return

在我们关注的代码执行时,先获取了C类中一个对应的函数对象然后将其强制转型为Funtion0 (性能损耗1: 保存lambda函数对象),然后进入在C中定义的Top-Level function open()中继续执行 (性能损耗2:方法调用),在 open() 中:

public static final <T> void open(com.maxtropy.viewtest.D, kotlin.jvm.functions.Function0<? extends T>);
    Code:
       0: aload_0
       1: ldc           #12                 // String d
       3: invokestatic  #18                 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
       6: aload_1
       7: ldc           #20                 // String body
       9: invokestatic  #18                 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
      12: aload_0
      13: invokevirtual #25                 // Method com/maxtropy/viewtest/D.open:()Z
      16: ifeq          26
      19: aload_1
      20: invokeinterface #31,  1           // InterfaceMethod kotlin/jvm/functions/Function0.invoke:()Ljava/lang/Object;
      25: pop
      26: return

我们发现,

  • 由于写的是 NotNull 的参数,首先会对入参进行非空的检查(性能损耗3:进行不必要的非空检查),在d.open 确认之后开始执行lamdba函数块中的方法,在这里也就是执行函数对象对应的 invoke()性能损耗4: 再次方法调用)。在 invoke() 方法中才是真正的对 foo1() foo2() 方法进行调用:
public final int invoke();
    Code:
       0: invokestatic  #23                 // Method com/maxtropy/viewtest/CKt.foo1:()I
       3: pop
       4: invokestatic  #26                 // Method com/maxtropy/viewtest/CKt.foo2:()I
       7: ireturn

哇!你猜Inline Function为我们做的性能优化 是不是不是很少 呢?

Non-local returns

由于我们知道在使用 inline 时,Kotlin编译器会自动帮我们消除lambda函数对应的enclosing对象。因此,想要从lambda函数中使用 return 关键字来退出调用inline函数的enclosing函数 (上面第一个例子中inline函数是Ckt.class中的open(), 调用open()函数的enclosing函数是C.class中的c() )是可以的.
官方把这叫作 non-local returns.
根本原理:每一个lambda函数都对应一个enclosing函数,不带label 的return只能退出一个函数。(在Kotlin中普通的return同在Java中一样,相对应的都是方法返回字节码,而方法返回字节码的字面意就是退出处于当前栈顶的执行方法。)Inline Function的lambda函数执行其实都在enclosing函数的闭包中,return退出lambda其实也就是退出enclosing函数。
!!!】从上面的例子也可以很明显的看到,lambda不能直接退出没被 inline 修饰的函数是因为lambda函数的执行都是在其 Function对象的invoke() 中,因此lambda中的return也仅仅只是退出 invoke() 罢了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343