簡述: 不知道是否有小夥伴還記得咱們以前的Effective Kotlin翻譯系列,以前一直忙於趕時髦研究Kotlin 1.3中的新特性。把此係列耽擱了,趕完時髦了仍是得踏實探究本質和基礎,從今天開始咱們將繼續探索Effective Kotlin系列,今天是Effective Kotlin第三講。java
翻譯說明:git
原標題: Effective Kotlin: Consider inline modifier for higher-order functionsgithub
原文地址: blog.kotlin-academy.com/effective-k…bash
原文做者: Marcin Moskalaapp
你或許已經注意到了全部集合操做的函數都是內聯的(inline)。你是否問過本身它們爲何要這麼定義呢? 例如,這是Kotlin標準庫中的filter
函數的簡化版本的源碼:ide
inline fun <T> Iterable<T>.filter(predicate: (T)->Boolean): List<T>{
val destination = ArrayList<T>()
for (element in this)
if (predicate(element))
destination.add(element)
return destination
}
複製代碼
這個inline
修飾符到底有多重要呢? 假設咱們有5000件商品,咱們須要對已經購買的商品累計算出總價。咱們能夠經過如下方式完成:函數
products.filter{ it.bought }.sumByDouble { it.price }
複製代碼
在個人機器上,運行上述代碼平均須要38毫秒。若是這個函數不是內聯的話會是多長時間呢? 不是內聯在個人機器上大概平均42毫秒。大家能夠本身檢查嘗試下,這裏是完整源碼. 這彷佛看起來差距不是很大,但每調用一次這個函數對集合進行處理時,你都會注意到這個時間差距大約爲10%左右。工具
當咱們修改lambda表達式中的局部變量時,能夠發現差距將會更大。對比下面兩個函數:post
inline fun repeat(times: Int, action: (Int) -> Unit) {
for (index in 0 until times) {
action(index)
}
}
fun noinlineRepeat(times: Int, action: (Int) -> Unit) {
for (index in 0 until times) {
action(index)
}
}
複製代碼
你可能已經注意到除了函數名不同以外,惟一的區別就是第一個函數使用inline
修飾符,而第二個函數沒有。用法也是徹底同樣的:性能
var a = 0
repeat(100_000_000) {
a += 1
}
var b = 0
noinlineRepeat(100_000_000) {
b += 1
}
複製代碼
上述代碼在執行時間上對比有很大的差別。內聯的repeat
函數平均運行時間是0.335ns, 而noinlineRepeat
函數平均運行時間是153980484.884ns。大概是內聯repeat
函數運行時間的466000倍! 大家能夠本身檢查嘗試下,這裏是完整源碼.
爲何這個如此重要呢? 這種性能的提高是否有其餘的成本呢? 咱們應該何時使用內聯(inline)修飾符呢?這些都是重點問題,咱們將盡力回答這些問題。然而這一切都須要從最基本的問題開始: 內聯修飾符到底有什麼做用?
咱們都知道函數一般是如何被調用的。先執行跳轉到函數體,而後執行函數體內全部的語句,最後跳回到最初調用函數的位置。
儘管強行對函數使用inline
修飾符標記,可是編譯器將會以不一樣的方式來對它進行處理。在代碼編譯期間,它用它的主體替換這樣的函數調用。 print
函數是inline
函數:
public inline fun print(message: Int) {
System.out.print(message)
}
複製代碼
當咱們在main函數中調用它時:
fun main(args: Array<String>) {
print(2)
print(2)
}
複製代碼
編譯後,它將變成下面這樣:
public static final void main(@NotNull String[] args) {
System.out.print(2)
System.out.print(2)
}
複製代碼
這裏有一點不同的是咱們不須要跳回到另外一個函數中。雖然這種影響能夠忽略不計。這就是爲何你定義這樣的內聯函數時會在IDEA IntelliJ中發出如下警告:
爲何IntelliJ建議咱們在含有lambda表達式做爲形參的函數中使用內聯呢?由於當咱們內聯函數體時,咱們不須要從參數中建立lambda表達式實例,而是能夠將它們內聯到函數調用中來。這個是上述repeat
函數的調用:
repeat(100) { println("A") }
複製代碼
將會編譯成這樣:
for (index in 0 until 1000) {
println("A")
}
複製代碼
正如你所看見的那樣,lambda表達式的主體println("A")
替換了內聯函數repeat
中action(index)
的調用。讓咱們看另外一外個例子。filter
函數的用法:
val products2 = products.filter { it.bought }
複製代碼
將被替換爲:
val destination = ArrayList<T>()
for (element in this)
if (predicate(element))
destination.add(element)
val products2 = destination
複製代碼
這是一項很是重要的改進。這是由於JVM自然地不支持lambda表達式。說清楚lambda表達式是如何被編譯的是件很複雜的事。但總的來講,有兩種結果:
咱們來看個例子。咱們有如下lambda表達式:
val lambda: ()->Unit = {
// body
}
複製代碼
它變成了JVM中的匿名類:
// Java
Function0 lambda = new Function0() {
public Object invoke() {
// code
}
};
複製代碼
或者它變成了單獨的文件中定義的普通類:
// Java
// Additional class in separate file
public class TestInlineKt$lambda implements Function0 {
public Object invoke() {
// code
}
}
// Usage
Function0 lambda = new TestInlineKt$lambda()
複製代碼
第二種效率更高,咱們儘量使用這種。僅僅當咱們須要使用局部變量時,第一種纔是必要的。
這就是爲何當咱們修改局部變量時,repeat
和noinlineRepeat
之間存在如此之大的運行速度差別的緣由。非內聯函數中的Lambda須要編譯爲匿名類。這是一個巨大的性能開銷,從而致使它們的建立和使用都較慢。當咱們使用內聯函數時,咱們根本不須要建立任何其餘類。本身檢查一下。編譯這段代碼並把它反編譯爲Java代碼:
fun main(args: Array<String>) {
var a = 0
repeat(100_000_000) {
a += 1
}
var b = 0
noinlineRepeat(100_000_000) {
b += 1
}
}
複製代碼
你會發現一些類似的東西:
/ Java public static final void main(@NotNull String[] args) {
int a = 0;
int times$iv = 100000000;
int var3 = 0;
for(int var4 = times$iv; var3 < var4; ++var3) {
++a;
}
final IntRef b = new IntRef();
b.element = 0;
noinlineRepeat(100000000, (Function1)(new Function1() {
public Object invoke(Object var1) {
++b.element;
return Unit.INSTANCE;
}
}));
}
複製代碼
在filter
函數例子中,使用內聯函數改進效果不是那麼明顯,這是由於lambda表達式在非內聯函數中是編譯成普通的類而非匿名類。因此它的建立和使用效率還算比較高,但仍有性能開銷,因此也就證實了最開始那個filter
例子爲何只有10%的運行速度差別。
內聯修飾符是一個很是關鍵的元素,它能使集合流處理的方式與基於循環的經典處理方式同樣高效。它通過一次又一次的測試,在代碼可讀性和性能方面已經優化到極點了,而且相比之下經典處理方式老是有很大的成本。例如,下面的代碼:
return data.filter { filterLoad(it) }.map { mapLoad(it) }
複製代碼
工做原理與下面代碼相同並具備相同的執行時間:
val list = ArrayList<String>()
for (it in data) {
if (filterLoad(it)) {
val value = mapLoad(it)
list.add(value)
}
}
return list
複製代碼
基準測量的具體結果(源碼在這裏):
Benchmark (size) Mode Cnt Score Error Units
filterAndMap 10 avgt 200 561.249 ± 1 ns/op
filterAndMap 1000 avgt 200 29803.183 ± 127 ns/op
filterAndMap 100000 avgt 200 3859008.234 ± 50022 ns/op
filterAndMapManual 10 avgt 200 526.825 ± 1 ns/op
filterAndMapManual 1000 avgt 200 28420.161 ± 94 ns/op
filterAndMapManual 100000 avgt 200 3831213.798 ± 34858 ns/op
複製代碼
從程序的角度來看,這兩個函數幾乎相同。儘管從可讀性的角度來看第一種方式要好不少,這就是爲何咱們應該老是寧願使用智能的集合流處理函數而不是本身去實現整個處理過程。此外若是stalib庫中集合處理函數不能知足咱們的需求時,請不要猶豫,本身動手編寫集合處理函數。例如,當我須要轉置集合中的集合時,這是我在上一個項目中添加的函數:
fun <E> List<List<E>>.transpose(): List<List<E>> {
if (isEmpty()) return this
val width = first().size
if (any { it.size != width }) {
throw IllegalArgumentException("All nested lists must have the same size, but sizes were ${map { it.size }}")
}
return (0 until width).map { col ->
(0 until size).map { row -> this[row][col] }
}
}
複製代碼
記得寫一些單元測試:
class TransposeTest {
private val list = listOf(listOf(1, 2, 3), listOf(4, 5, 6))
@Test
fun `Transposition of transposition is identity`() {
Assert.assertEquals(list, list.transpose().transpose())
}
@Test
fun `Simple transposition test`() {
val transposed = listOf(listOf(1, 4), listOf(2, 5), listOf(3, 6))
assertEquals(transposed, list.transpose())
}
}
複製代碼
內聯不該該被過分使用,由於它也是有成本的。我想在代碼中打印出更多的數字2, 因此我就定義了下面這個函數:
inline fun twoPrintTwo() {
print(2)
print(2)
}
複製代碼
這對我來講可能還不夠,因此我添加了這個函數:
inline fun twoTwoPrintTwo() {
twoPrintTwo()
twoPrintTwo()
}
複製代碼
仍是不滿意。我又定義瞭如下這兩個函數:
inline fun twoTwoTwoPrintTwo() {
twoTwoPrintTwo()
twoTwoPrintTwo()
}
fun twoTwoTwoTwoPrintTwo() {
twoTwoTwoPrintTwo()
twoTwoTwoPrintTwo()
}
複製代碼
而後我決定檢查編譯後的代碼中發生了什麼,因此我將編譯爲JVM字節碼而後將它反編譯成Java代碼。twoTwoPrintTwo
函數已經很長了:
public static final void twoTwoPrintTwo() {
byte var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
}
複製代碼
可是twoTwoTwoTwoPrintTwo
就更加恐怖了
public static final void twoTwoTwoTwoPrintTwo() {
byte var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
var1 = 2;
System.out.print(var1);
}
複製代碼
這說明了內聯函數的主要問題: 當咱們過分使用它們時,會使得代碼體積不斷增大。這實際上就是爲何當咱們使用他們時IntelliJ會給出警告提示。
內聯修飾符由於它特殊的語法特性而發生的變化遠遠超過咱們在本篇文章中看到的內容。它能夠實化泛型類型。可是它也有一些侷限性。雖然這與Effective Kotlin系列無關而且屬因而另一個話題。若是你想要我闡述更多有關它,請在Twitter或評論中表達你的想法。
咱們使用內聯修飾符時最多見的場景就是把函數做爲另外一個函數的參數時(高階函數)。集合或字符串處理(如filter
,map
或者joinToString
)或者一些獨立的函數(如repeat
)就是很好的例子。
這就是爲何inline
修飾符常常被庫開發人員用來作一些重要優化的緣由了。他們應該知道它是如何工做的,哪裏還須要被改進以及使用成本是什麼。當咱們使用函數類型做爲參數來定義本身的工具類函數時,咱們也須要在項目中使用inline
修飾符。當咱們沒有函數類型做爲參數,沒有reified實化類型參數而且也不須要非本地返回時,那麼咱們極可能不該該使用inline
修飾符了。這就是爲何咱們在非上述狀況下使用inline
修飾符會在Android Studio或IDEA IntelliJ獲得一個警告緣由。
這是Effective Kotlin系列第三篇文章,講得是inline
內聯函數存在使用時潛在隱患,一旦使用不當或者過分使用就會形成性能上損失。基於這一點原做者從發現問題到剖析整個inline內聯函數原理以及最後如何去選擇在哪一種場景下使用內聯函數。我相信有了這篇文章,你對Kotlin中的內聯函數應該是瞭然於胸了吧。後面會繼續Effective Kotlin翻譯系列,歡迎繼續關注~~~
Effective Kotlin翻譯系列
原創系列:
翻譯系列:
實戰系列:
歡迎關注Kotlin開發者聯盟,這裏有最新Kotlin技術文章,每週會不按期翻譯一篇Kotlin國外技術文章。若是你也喜歡Kotlin,歡迎加入咱們~~~