End

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

本文地址


目录

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

迭代过程:

  • 1.0 版本:使用命令式风格的代码实现基础功能
  • 2.0 版本:利用扩展函数高阶函数优化代码,实现函数式风格的代码
  • 3.0 版本:使用 inline 提升软件的性能,并分析高阶函数的实现原理

命令式风格

统计步骤:

  • 文本清洗:将标点符号替换成空格
  • 文本分割:以空格作为分隔符,对文本进行分割
  • 词频统计:遍历 List,将单词频率存储在 Map 中
  • 词频排序:将高频词放在前面,低频词放在后面
data class WordFreq(val word: String, val frequency: Int) // 数据类

class TextProcessorV1 {
    fun processText(text: String): List<WordFreq> {
        val cleaned = clean(text)
        val words = cleaned.split(" ")
        val map = getWordCount(words)
        val list = sortByFrequency(map)
        return list
    }

    // 统计文件当中的单词词频
    fun processFile(file: File): List<WordFreq> {
        val text = file.readText(Charsets.UTF_8) // 从文件里读取文本
        return processText(text)
    }
}

文本清洗

fun clean(text: String): String {
    return text.replace(".", " ")
        .replace("!", " ")
        .replace(",", " ")
        .replace("?", " ")
        .replace("'", " ")
        .replace("*", " ")
        .replace("#", " ")
        .replace("@", " ")
        .trim()
}

使用正则表达式,将所有不是英文字母的字符都替换成空格:

fun clean(text: String) = text.replace("[^A-Za-z]".toRegex(), " ").trim()

词频统计

用一个 Map 来存储单词和它对应频率:

fun getWordCount(list: List<String>): Map<String, Int> {
    val map = hashMapOf<String, Int>()
    for (word in list) {
        if (word == "") continue
        val trimWord = word.trim()
        val count = map.getOrDefault(trimWord, 0)
        map[trimWord] = count + 1
    }
    return map
}

词频排序

将无序的数据结构换成有序的。

fun sortByFrequency(map: Map<String, Int>): MutableList<WordFreq> {
    val list = mutableListOf<WordFreq>()
    for (entry in map) {
        if (entry.key == "") continue
        val freq = WordFreq(entry.key, entry.value)
        list.add(freq)
    }
    list.sortByDescending { it.frequency } // 以词频降序排序
    return list
}

函数式风格

Kotlin 既有命令式的一面,也有函数式的一面,它们有着各自擅长的领域。

借助函数式编程的思想来优化代码:

fun processText(text: String): List<WordFreq> {
    return text
        .clean()
        .split(" ")
        .getWordCount()
        .sortByFrequency { WordFreq(it.key, it.value) }
}

转为扩展方法

fun clean(t: String) = t.replace("[^A-Za-z]".toRegex(), " ").trim() // 普通函数

fun String.clean() = replace("[^A-Za-z]".toRegex(), " ").trim()     // 扩展方法

真实项目中,String.clean() 使用顶层扩展并不合适,顶层扩展只适用于通用的逻辑,否则不清楚的人看着 idea 提示的扩展函数也是一脸懵逼。

private fun List<String>.getWordCount(): Map<String, Int> {
    val map = HashMap<String, Int>()
    for (element in this) { // 原本的参数 list 集合变成了 this
        if (element == "") continue
        val trim = element.trim()
        val count = map.getOrDefault(trim, 0)
        map[trim] = count + 1
    }
    return map
}
private fun Map<String, Int>.sortByFrequency(): MutableList<WordFreq> {
    val list = mutableListOf<WordFreq>()
    for (entry in this) { // 将参数变成 this
        val freq = WordFreq(entry.key, entry.value)
        list.add(freq)
    }

    list.sortByDescending { it.frequency }
    return list
}

对 sortByFrequency() 的逻辑进行拆分:

private fun <T> Map<String, Int>.mapToList(transform: (Map.Entry<String, Int>) -> T): MutableList<T> {
    val list = mutableListOf<T>()
    for (entry in this) {
        val freq = transform(entry) // 调用 transform,将 Map.Entry 类型的参数转换为 T
        list.add(freq)
    }
    return list
}

优化后的效果

fun processText(text: String): List<WordFreq> = text
    .clean()
    .split(" ")
    .getWordCount()
    .mapToList { WordFreq(it.key, it.value) }
    .sortedByDescending { it.frequency }

进一步优化

上面的 clean、getWordCount、mapToList 等方法,可以借助 Kotlin 标准库函数来实现:

fun processText(text: String): List<WordFreq> = text
    .replace("[^A-Za-z]".toRegex(), " ")
    .trim()
    .split(" ")
    .filter { it != "" }
    .groupBy { it }
    .map { WordFreq(it.key, it.value.size) }
    .sortedByDescending { it.frequency }

使用 inline 优化

这个版本,我们只需要为 mapToList 这个高阶函数增加一个 inline 关键字即可。

private inline fun <T> Map<String, Int>.mapToList(
    transform: (Map.Entry<String, Int>) -> T
): MutableList<T> {
    //...
}

高阶函数的实现原理

// HigherOrderExample.kt
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 又是个什么东西?

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

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

inline 的实现原理

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

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

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

反编译后的 Java 后:

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

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

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

使用 JMH 测试 inline 性能

不使用 inline 时:

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

为了验证使用 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 函数类型参数的函数

另外,在 processText() 方法的内部,getWordCount() 和 mapToList() 这两个方法还会报错:

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

报错的原因是:inline 的作用其实就是将 inline 函数当中的代码拷贝到调用处,而只有 public 的方法才可以被拷贝(访问))。

2016-06-01

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