kapan harus menggunakan fungsi inline di Kotlin?

105

Saya tahu bahwa fungsi sebaris mungkin akan meningkatkan kinerja & menyebabkan kode yang dihasilkan bertambah, tetapi saya tidak yakin kapan benar untuk menggunakannya.

lock(l) { foo() }

Alih-alih membuat objek fungsi untuk parameter dan membuat panggilan, kompilator dapat mengeluarkan kode berikut. ( Sumber )

l.lock()
try {
  foo()
}
finally {
  l.unlock()
}

tetapi saya menemukan bahwa tidak ada objek fungsi yang dibuat oleh kotlin untuk fungsi non-inline. Mengapa?

/**non-inline function**/
fun lock(lock: Lock, block: () -> Unit) {
    lock.lock();
    try {
        block();
    } finally {
        lock.unlock();
    }
}
holi-java
sumber
7
Ada dua kasus penggunaan utama untuk ini, satu untuk tipe tertentu dari fungsi orde tinggi, dan yang lainnya adalah parameter tipe reified. Dokumentasi fungsi inline meliputi: kotlinlang.org/docs/reference/inline-functions.html
zsmb13
2
@ zb13 makasih pak. tetapi saya tidak mengerti bahwa: "Alih-alih membuat objek fungsi untuk parameter dan menghasilkan panggilan, kompiler dapat memancarkan kode berikut"
holi-java
2
saya juga tidak mendapatkan contoh itu tbh.
filthy_wizard

Jawaban:

279

Katakanlah Anda membuat fungsi orde tinggi yang menggunakan lambda tipe () -> Unit(tanpa parameter, tanpa nilai kembalian), dan mengeksekusinya seperti ini:

fun nonInlined(block: () -> Unit) {
    println("before")
    block()
    println("after")
}

Dalam bahasa Java, ini akan diterjemahkan menjadi sesuatu seperti ini (disederhanakan!):

public void nonInlined(Function block) {
    System.out.println("before");
    block.invoke();
    System.out.println("after");
}

Dan ketika Anda menyebutnya dari Kotlin ...

nonInlined {
    println("do something here")
}

Di bawah tenda, sebuah instance dari Functionakan dibuat di sini, yang membungkus kode di dalam lambda (sekali lagi, ini disederhanakan):

nonInlined(new Function() {
    @Override
    public void invoke() {
        System.out.println("do something here");
    }
});

Jadi pada dasarnya, memanggil fungsi ini dan meneruskan lambda ke sana akan selalu membuat instance dari suatu Functionobjek.


Di sisi lain, jika Anda menggunakan inlinekata kunci:

inline fun inlined(block: () -> Unit) {
    println("before")
    block()
    println("after")
}

Bila Anda menyebutnya seperti ini:

inlined {
    println("do something here")
}

Tidak ada Functioninstance yang akan dibuat, sebagai gantinya, kode di sekitar pemanggilan blockdi dalam fungsi inline akan disalin ke situs panggilan, jadi Anda akan mendapatkan sesuatu seperti ini di bytecode:

System.out.println("before");
System.out.println("do something here");
System.out.println("after");

Dalam kasus ini, tidak ada instance baru yang dibuat.

zsmb13
sumber
19
Apa keuntungan memiliki pembungkus objek Fungsi di tempat pertama? yaitu - mengapa tidak semuanya inline?
Arturs Vancans
14
Dengan cara ini Anda juga dapat secara sewenang-wenang meneruskan fungsi sebagai parameter, menyimpannya dalam variabel, dll.
zsmb13
6
Penjelasan bagus oleh @ zsmb13
Yajairo87
2
Anda bisa, dan jika Anda melakukan hal-hal rumit dengannya, Anda akhirnya akan ingin tahu tentang kata kunci noinlinedan crossinline- lihat dokumennya .
zsmb13
2
Dokumen memberikan alasan Anda tidak ingin menyebaris secara default: Penyisipan dapat menyebabkan kode yang dihasilkan bertambah; namun, jika kita melakukannya dengan cara yang wajar (yaitu menghindari penyejajaran fungsi besar), kinerja akan terbayar, terutama pada situs panggilan "megamorfik" di dalam loop.
CorayThan
43

Izinkan saya menambahkan: "Kapan tidak digunakan inline" :

1) Jika Anda memiliki fungsi sederhana yang tidak menerima fungsi lain sebagai argumen, tidak masuk akal untuk menyebariskannya. IntelliJ akan memperingatkan Anda:

Dampak kinerja yang diharapkan dari inlining '...' tidak signifikan. Sebaris berfungsi paling baik untuk fungsi dengan parameter jenis fungsional

2) Bahkan jika Anda memiliki fungsi "dengan parameter tipe fungsional", Anda mungkin menemukan kompilator yang memberi tahu Anda bahwa inlining tidak berfungsi. Pertimbangkan contoh ini:

inline fun calculateNoInline(param: Int, operation: IntMapper): Int {
    val o = operation //compiler does not like this
    return o(param)
}

Kode ini tidak dapat dikompilasi dengan kesalahan:

Penggunaan ilegal parameter inline 'operation' di '...'. Tambahkan pengubah 'noinline' ke deklarasi parameter.

Alasannya adalah kompilator tidak dapat menyebariskan kode ini. Jika operationtidak dibungkus dalam sebuah objek (yang diimplikasikan oleh inlinekarena Anda ingin menghindari ini), bagaimana itu dapat ditugaskan ke variabel sama sekali? Dalam kasus ini, kompilator menyarankan untuk membuat argumen noinline. Memiliki inlinefungsi dengan satu noinlinefungsi tidak masuk akal, jangan lakukan itu. Namun, jika ada beberapa parameter jenis fungsional, pertimbangkan untuk membuat sebaris beberapa di antaranya jika diperlukan.

Berikut beberapa aturan yang disarankan:

  • Anda bisa sebaris ketika semua parameter tipe fungsional dipanggil secara langsung atau diteruskan ke fungsi sebaris lainnya
  • Anda harus sebaris jika ^ adalah kasusnya.
  • Anda tidak bisa sebaris ketika parameter fungsi sedang ditugaskan ke variabel di dalam fungsi
  • Anda harus mempertimbangkan penyebarisan jika setidaknya salah satu parameter jenis fungsional Anda dapat disebariskan, gunakan noinlineuntuk yang lain.
  • Anda tidak boleh menyebariskan fungsi besar, pikirkan tentang kode byte yang dihasilkan. Ini akan disalin ke semua tempat asal pemanggilan fungsi.
  • Kasus penggunaan lainnya adalah reifiedparameter tipe, yang mengharuskan Anda untuk menggunakannya inline. Baca disini .
s1m0nw1
sumber
4
secara teknis Anda masih bisa fungsi inline yang tidak mengambil ekspresi lambda kan? .. di sini keuntungannya adalah bahwa function call overhead dihindari dalam kasus itu .. bahasa seperti Scala mengizinkan ini .. tidak yakin mengapa Kotlin melarang jenis inline- ing
rogue-one
3
@ rogue-one Kotlin tidak melarang penyebarisan kali ini. Penulis bahasa hanya mengklaim bahwa manfaat kinerja kemungkinan besar tidak signifikan. Metode kecil sudah cenderung di-inline oleh JVM selama pengoptimalan JIT, terutama jika metode tersebut sering dijalankan. Kasus lain yang inlinebisa berbahaya adalah ketika parameter fungsional dipanggil beberapa kali dalam fungsi sebaris, seperti di cabang bersyarat yang berbeda. Saya baru saja mengalami kasus di mana semua bytecode untuk argumen fungsional diduplikasi karena ini.
Mike Hill
5

Kasus yang paling penting ketika kita menggunakan pengubah inline adalah ketika kita mendefinisikan fungsi mirip-util dengan fungsi parameter. Pemrosesan koleksi atau string (seperti filter, mapatau joinToString) atau hanya fungsi yang berdiri sendiri adalah contoh sempurna.

Inilah sebabnya mengapa pengubah sebaris sebagian besar merupakan pengoptimalan penting bagi pengembang perpustakaan. Mereka harus tahu cara kerjanya dan apa saja perbaikan dan biayanya. Kita harus menggunakan pengubah inline dalam proyek kita ketika kita mendefinisikan fungsi util kita sendiri dengan parameter jenis fungsi.

Jika kita tidak memiliki parameter tipe fungsi, parameter tipe reified, dan kita tidak membutuhkan pengembalian non-lokal, kemungkinan besar kita tidak boleh menggunakan pengubah inline. Inilah mengapa kami akan memiliki peringatan di Android Studio atau IDEA IntelliJ.

Juga, ada masalah ukuran kode. Menyebariskan fungsi yang besar dapat secara dramatis meningkatkan ukuran bytecode karena disalin ke setiap situs panggilan. Dalam kasus seperti itu, Anda dapat memfaktor ulang fungsi dan mengekstrak kode ke fungsi biasa.

0xAliHn
sumber
4

Fungsi tingkat tinggi sangat membantu dan benar-benar dapat meningkatkan reusabilitykode. Namun, salah satu kekhawatiran terbesar tentang penggunaannya adalah efisiensi. Ekspresi Lambda dikompilasi ke kelas (seringkali kelas anonim), dan pembuatan objek di Java adalah operasi yang berat. Kita masih dapat menggunakan fungsi tingkat tinggi dengan cara yang efektif, sambil menjaga semua manfaat, dengan membuat fungsi sebaris.

inilah fungsi inline ke dalam gambar

Ketika suatu fungsi ditandai sebagai inline, selama kompilasi kode, kompilator akan mengganti semua panggilan fungsi dengan badan fungsi yang sebenarnya. Selain itu, ekspresi lambda yang diberikan sebagai argumen diganti dengan tubuh aslinya. Mereka tidak akan diperlakukan sebagai fungsi, tetapi sebagai kode aktual.

Singkatnya: - Inline -> daripada dipanggil, mereka digantikan oleh kode badan fungsi pada waktu kompilasi ...

Di Kotlin, menggunakan fungsi sebagai parameter fungsi lain (disebut fungsi tingkat tinggi) terasa lebih alami daripada di Java.

Namun, penggunaan lambda memiliki beberapa kelemahan. Karena mereka adalah kelas anonim (dan karenanya, objek), mereka memerlukan memori (dan bahkan mungkin menambah jumlah metode keseluruhan aplikasi Anda). Untuk menghindari hal ini, kita dapat menggunakan metode kita sebaris.

fun notInlined(getString: () -> String?) = println(getString())

inline fun inlined(getString: () -> String?) = println(getString())

Dari contoh di atas : - Kedua fungsi ini melakukan hal yang persis sama - mencetak hasil dari fungsi getString. Yang satu sejajar dan yang lainnya tidak.

Jika Anda memeriksa kode java yang telah didekompilasi, Anda akan melihat bahwa metodenya benar-benar identik. Itu karena kata kunci inline adalah instruksi kepada kompilator untuk menyalin kode ke dalam situs panggilan.

Namun, jika kita meneruskan tipe fungsi apa pun ke fungsi lain seperti di bawah ini:

//Compile time error… Illegal usage of inline function type ftOne...
 inline fun Int.doSomething(y: Int, ftOne: Int.(Int) -> Int, ftTwo: (Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/
 }

Untuk mengatasinya, kita bisa menulis ulang fungsi kita seperti di bawah ini:

inline fun Int.doSomething(y: Int, noinline ftOne: Int.(Int) -> Int, ftTwo: (Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/}

Misalkan kita memiliki fungsi urutan yang lebih tinggi seperti di bawah ini:

inline fun Int.doSomething(y: Int, noinline ftOne: Int.(Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/}

Di sini, kompilator akan memberi tahu kita untuk tidak menggunakan kata kunci inline ketika hanya ada satu parameter lambda dan kita meneruskannya ke fungsi lain. Jadi, kita bisa menulis ulang fungsi di atas seperti di bawah ini:

fun Int.doSomething(y: Int, ftOne: Int.(Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/
}

Catatan : -kami harus menghapus kata kunci noinline juga karena hanya dapat digunakan untuk fungsi inline!

Misalkan kita memiliki fungsi seperti ini ->

fun intercept() {
    // ...
    val start = SystemClock.elapsedRealtime()
    val result = doSomethingWeWantToMeasure()
    val duration = SystemClock.elapsedRealtime() - start
    log(duration)
    // ...}

Ini berfungsi dengan baik tetapi inti logika fungsi tersebut tercemar dengan kode pengukuran sehingga mempersulit kolega Anda untuk mengerjakan apa yang terjadi. :)

Inilah bagaimana fungsi sebaris dapat membantu kode ini:

      fun intercept() {
    // ...
    val result = measure { doSomethingWeWantToMeasure() }
    // ...
    }

     inline fun <T> measure(action: () -> T) {
    val start = SystemClock.elapsedRealtime()
    val result = action()
    val duration = SystemClock.elapsedRealtime() - start
    log(duration)
    return result
    }

Sekarang saya dapat berkonsentrasi untuk membaca apa maksud utama fungsi intercept () tanpa melewatkan baris kode pengukuran. Kami juga mendapat manfaat dari opsi untuk menggunakan kembali kode itu di tempat lain yang kami inginkan

inline memungkinkan Anda memanggil fungsi dengan argumen lambda dalam closure ({...}) daripada meneruskan lambda like measure (myLamda)

Wini
sumber
2

Satu kasus sederhana di mana Anda mungkin menginginkannya adalah ketika Anda membuat fungsi util yang mengambil blok penangguhan. Pertimbangkan ini.

fun timer(block: () -> Unit) {
    // stuff
    block()
    //stuff
}

fun logic() { }

suspend fun asyncLogic() { }

fun main() {
    timer { logic() }

    // This is an error
    timer { asyncLogic() }
}

Dalam kasus ini, timer kami tidak akan menerima fungsi penangguhan. Untuk mengatasinya, Anda mungkin tergoda untuk membuatnya ditangguhkan juga

suspend fun timer(block: suspend () -> Unit) {
    // stuff
    block()
    // stuff
}

Tapi kemudian hanya bisa digunakan dari fungsi coroutine / suspend itu sendiri. Kemudian Anda akan membuat versi asinkron dan versi non-asinkron dari utils ini. Masalahnya hilang jika Anda membuatnya sejajar.

inline fun timer(block: () -> Unit) {
    // stuff
    block()
    // stuff
}

fun main() {
    // timer can be used from anywhere now
    timer { logic() }

    launch {
        timer { asyncLogic() }
    }
}

Berikut adalah taman bermain kotlin dengan status error. Buat pengatur waktu sejajar untuk menyelesaikannya.

Anthony Naddeo
sumber