End

Kotlin 朱涛-8 实战 inline 函数式编程 词频统计

本文地址


目录

08 | 实战:用Kotlin写一个英语词频统计程序

高阶函数的实现原理

fun foo(block: () -> Unit) {
    block()
}

fun main() {
    var i = 0
    foo { i++ }
}

反编译成 Java 后:

public final class HigherOrderExampleKt {
    public static final void foo(Function0 block) { // 参数是一个接口
        block.invoke();
    }

    public static final void main() {
        int i = 0;
        foo((Function0) (new Function0() { // 参数是一个实现 Function0 接口的【匿名内部类】
            public final void invoke() {   // 接口中定义的方法,这个方法【没有参数】
                i++;
            }
        }));
    }
}

可以看到,Kotlin 高阶函数当中的函数类型参数,变成了 Function0,而 main() 函数当中的高阶函数调用,也变成了匿名内部类的调用方式。

Function0 是 Kotlin 标准库当中定义的接口,它代表没有参数的函数类型。Kotlin 一共定义了 23 个类似的接口,从 Function0 一直到 Function22,分别代表了无参数的函数类型22 个参数的函数类型

public interface Function0<out R> : Function<R> {
    public operator fun invoke(): R
}

inline 的实现原理

使用 inline 优化过的高阶函数:

inline fun foo(block: () -> Unit) { // 唯一的区别:多了一个关键字 inline
    block()
}

fun main() {
    var i = 0
    foo { i++ }
}

反编译后的 Java 后:

public final class HigherOrderInlineExampleKt {
    public static final void foo(Function0 block) { // 没有变化
        block.invoke();
    }

    public static final void main() {
        int i = 0;      // 区别:匿名内部类不见了,方法调用也不见了
        int i = i + 1;  // 原理:将 inline 函数当中的代码拷贝到调用处
    }
}

inline 的作用其实就是:将 inline 函数当中的代码拷贝到调用处

inline 性能

不使用 inline 时:

  • main() 中需要调用 foo(),多了一次函数调用的开销
  • foo() 中需要创建了匿名内部类对象,这也是额外的开销

使用 JMH 测试 inline 性能

为了验证使用 inline 前后的性能差异,我们可以使用 JMH(Java Microbenchmark Harness)对这两组代码进行性能测试。JMH 可以最大程度地排除外界因素的干扰(比如内存抖动、虚拟机预热),从而判断出我们这两组代码执行效率的差异。它的结果不一定非常精确,但足以说明一些问题。

下面以两组测试代码为例,来探究下 inline 到底能为我们带来多少性能上的提升:

fun foo(block: () -> Unit) = block()              // 不用 inline 的高阶函数
inline fun fooInline(block: () -> Unit) = block() // 使用 inline 的高阶函数

// 测试无 inline 的代码
@Benchmark
fun testNonInlined() {
    var i = 0
    foo { i++ }
}

// 测试有 inline 的代码
@Benchmark
fun testInlined() {
    var i = 0
    fooInline { i++ }
}

最终的测试结果如下,分数越高性能越好:

Benchmark        Mode          Score         Error     Units
testInlined      thrpt   3272062.466   ± 67403.033    ops/ms
testNonInlined   thrpt    355450.945   ± 12647.220    ops/ms

从上面的测试结果我们能看出来,是否使用 inline 的效率相差 10 倍

测试多层嵌套的性能差异

为了模拟复杂的代码结构,我们可以简单地将上面这两个函数分别嵌套 10 个层级,然后看看它们之间的性能差异:

@Benchmark
fun testNonInlined() {
    var i = 0
    foo {
        foo {
            foo {
                foo {
                    foo {
                        foo {
                            foo {
                                foo {
                                    foo {
                                        foo {
                                            i++
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

@Benchmark
fun testInlined() {
    var i = 0
    fooInline {
        fooInline {
            fooInline {
                fooInline {
                    fooInline {
                        fooInline {
                            fooInline {
                                fooInline {
                                    fooInline {
                                        fooInline {
                                            i++
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
Benchmark         Mode         Score         Error    Units
testInlined      thrpt   3266143.092   ± 85861.453   ops/ms
testNonInlined   thrpt     31404.262   ±   804.615   ops/ms

可以看到,在嵌套了 10 个层级以后,testInlined 的性能几乎没有什么变化;而 testNonInlined 的性能比 1 层嵌套差了 10 倍。

在这种情况下,testInlined() 与 testNonInlined() 之间的性能差异就达到了 100 倍

如果反编译成 Java 代码,能看到:

  • 对于 testNonInlined(),由于 foo() 嵌套了 10 层,它反编译后的代码也嵌套了 10 层函数调用,中间还伴随了 10 次匿名内部类的创建
  • 而对于 testInlined(),则只有简单的两行代码,完全没有任何嵌套的痕迹

inline 的使用限制

Kotlin 官方建议:仅将 inline 用于修饰高阶函数。而且,也不是所有高阶函数都可以用 inline

IntelliJ IDEA 会对使用 inline 修饰的普通函数发出警告:

Expected 期望 performance impact 性能影响 of inlining is insignificant 微不足道. Inlining works best for functions with parameters of functional types 函数类型参数的函数

另外,由于 inline 的作用其实就是将 inline 函数当中的代码拷贝到调用处,所以只有 public 的方法才可以使用 inline。

Public-API inline function cannot access non-public API xxx

2016-06-01

posted @ 2016-06-01 13:09  白乾涛  阅读(5266)  评论(0编辑  收藏  举报