Kelemahan JIT Compiler: Mengapa Harus Menghindari NYI?

API7.ai

September 30, 2022

OpenResty (NGINX + Lua)

Pada artikel sebelumnya, kita telah membahas FFI di LuaJIT. Jika proyek Anda hanya menggunakan API yang disediakan oleh OpenResty dan Anda tidak perlu memanggil fungsi C, maka FFI tidak terlalu penting bagi Anda. Anda hanya perlu memastikan bahwa lua-resty-core diaktifkan.

Namun, NYI di LuaJIT, yang akan kita bahas hari ini, adalah masalah penting yang tidak dapat dihindari oleh setiap insinyur yang menggunakan OpenResty, dan sangat memengaruhi performa.

Anda dapat dengan cepat menulis kode yang benar secara logis menggunakan OpenResty, tetapi tanpa memahami NYI, Anda tidak dapat menulis kode yang efisien dan tidak dapat memanfaatkan kekuatan OpenResty. Perbedaan performa antara keduanya setidaknya satu orde magnitudo.

Apa itu NYI?

Mari kita mulai dengan mengingat kembali poin yang telah kita bahas sebelumnya.

Runtime LuaJIT, selain memiliki implementasi assembler dari interpreter Lua, juga memiliki kompiler JIT yang dapat menghasilkan kode mesin secara langsung.

Implementasi kompiler JIT di LuaJIT belum sepenuhnya selesai. Kompiler ini tidak dapat mengompilasi beberapa fungsi karena sulit untuk diimplementasikan dan karena penulis LuaJIT saat ini sudah semi-pensiun. Ini termasuk fungsi umum seperti pairs(), fungsi unpack(), modul Lua C yang berbasis implementasi Lua CFunction, dan sebagainya. Hal ini memungkinkan kompiler JIT untuk kembali ke mode interpreter ketika menemukan operasi yang tidak didukung pada jalur kode saat ini.

Situs resmi LuaJIT memiliki daftar lengkap NYI ini, dan saya menyarankan Anda untuk membacanya. Tujuan artikel ini bukan untuk menghafal daftar ini, tetapi untuk mengingatkannya secara sadar saat menulis kode.

Di bawah ini, saya telah mengambil beberapa fungsi dari daftar NYI untuk pustaka string.

string library

Status kompilasi string.byte adalah ya, yang berarti dapat dioptimalkan dengan JIT, dan Anda dapat menggunakannya dalam kode Anda tanpa khawatir.

Status kompilasi string.char adalah 2.1, yang berarti telah didukung sejak LuaJIT 2.1. Seperti yang kita tahu, LuaJIT di OpenResty berbasis LuaJIT 2.1, jadi Anda dapat menggunakannya dengan aman.

Status kompilasi string.dump adalah never, artinya tidak akan dioptimalkan dengan JIT dan akan kembali ke mode interpreter. Sampai saat ini, tidak ada rencana untuk mendukung ini di masa depan.

string.find memiliki status kompilasi 2.1 partial, yang berarti didukung sebagian sejak LuaJIT 2.1, dan catatan setelahnya menyatakan bahwa hanya mendukung pencarian string tetap, bukan pencocokan pola. Jadi untuk menemukan string tetap, string.find dapat dioptimalkan dengan JIT.

Secara alami, kita harus menghindari penggunaan NYI agar lebih banyak kode kita dapat dikompilasi JIT dan performa dapat dijamin. Namun, dalam lingkungan nyata, kita terkadang tidak dapat menghindari penggunaan beberapa fungsi NYI, jadi apa yang harus kita lakukan?

Alternatif untuk NYI

Jangan khawatir. Sebagian besar fungsi NYI dapat kita tinggalkan dengan hormat dan mengimplementasikan fungsionalitasnya dengan cara lain. Selanjutnya, saya telah memilih beberapa NYI yang khas untuk dijelaskan dan memandu Anda melalui berbagai jenis alternatif NYI. Dengan cara ini, Anda juga dapat mempelajari NYI lainnya.

string.gsub()

Mari kita lihat fungsi string.gsub() terlebih dahulu, yang merupakan fungsi manipulasi string bawaan Lua yang melakukan penggantian string global, seperti contoh berikut.

$ resty -e 'local new = string.gsub("banana", "a", "A"); print(new)' bAnAnA

Fungsi ini adalah fungsi NYI dan tidak dapat dikompilasi oleh JIT.

Kita dapat mencoba mencari fungsi pengganti di API OpenResty, tetapi bagi kebanyakan orang, tidak praktis untuk mengingat semua API dan penggunaannya. Itulah mengapa saya selalu membuka halaman dokumentasi GitHub untuk lua-nginx-module dalam pekerjaan pengembangan saya.

Misalnya, kita dapat menggunakan gsub sebagai kata kunci untuk mencari halaman dokumentasi, dan ngx.re.gsub akan muncul.

Kita juga dapat menggunakan alat restydoc yang direkomendasikan sebelumnya untuk mencari API OpenResty. Anda dapat mencoba menggunakannya untuk mencari gsub.

$ restydoc -s gsub

Seperti yang Anda lihat, alih-alih mengembalikan ngx.re.gsub yang kita harapkan, fungsi Lua ditampilkan. Sebenarnya, pada tahap ini, restydoc mengembalikan kecocokan unik yang tepat, sehingga lebih cocok digunakan jika Anda mengetahui nama API secara eksplisit. Untuk pencarian fuzzy, Anda masih harus melakukannya secara manual di dokumentasi.

Kembali ke hasil pencarian, kita melihat bahwa definisi fungsi ngx.re.gsub adalah sebagai berikut:

newstr, n, err = ngx.re.gsub(subject, regex, replace, options?)

Di sini, parameter fungsi dan nilai kembalian diberi nama dengan makna spesifik. Sebenarnya, di OpenResty, saya tidak menyarankan Anda untuk menulis banyak komentar. Sebagian besar waktu, nama yang baik lebih baik daripada beberapa baris komentar.

Bagi insinyur yang tidak terbiasa dengan sistem reguler OpenResty, Anda mungkin bingung ketika melihat variabel options di akhir. Namun, penjelasan variabel ini tidak ada dalam fungsi ini tetapi dalam dokumentasi untuk fungsi ngx.re.match.

Jika Anda melihat dokumentasi untuk options, Anda akan melihat bahwa jika kita mengaturnya ke jo, itu akan mengaktifkan PCRE JIT, sehingga kode yang menggunakan ngx.re.gsub dapat dikompilasi JIT oleh LuaJIT dan juga oleh PCRE JIT.

Saya tidak akan membahas detail dokumentasi. Dokumentasi OpenResty sangat baik, jadi bacalah dengan cermat dan Anda dapat menyelesaikan sebagian besar masalah Anda.

string.find()

Tidak seperti string.gsub, string.find dapat di-JIT dalam mode plain (yaitu pencarian string), sedangkan string.find tidak dapat di-JIT untuk pencarian string dengan regularitas, yang dilakukan menggunakan API OpenResty ngx.re.find.

Jadi, ketika Anda melakukan pencarian string di OpenResty, Anda harus terlebih dahulu membedakan dengan jelas apakah Anda mencari string tetap atau ekspresi reguler. Jika yang pertama, gunakan string.find dan ingat untuk mengatur plain ke true di akhir.

string.find("foo bar", "foo", 1, true)

Dalam kasus yang kedua, Anda harus menggunakan API OpenResty dan mengaktifkan opsi JIT untuk PCRE.

ngx.re.find("foo bar", "^foo", "jo")

Akan lebih tepat untuk membuat lapisan pembungkus di sini dan mengaktifkan opsi optimasi secara default, tanpa membiarkan pengguna akhir mengetahui begitu banyak detail. Dengan cara itu, itu adalah fungsi pencarian string yang seragam ke luar. Seperti yang Anda rasakan, terkadang terlalu banyak opsi dan terlalu banyak fleksibilitas bukanlah hal yang baik.

unpack()

Fungsi ketiga yang akan kita lihat adalah unpack(). unpack() juga merupakan fungsi yang perlu dihindari, terutama tidak dalam tubuh loop. Sebagai gantinya, Anda dapat mengaksesnya menggunakan nomor indeks array, seperti dalam contoh dari kode berikut.

$ resty -e ' local a = {100, 200, 300, 400} for i = 1, 2 do print(unpack(a)) end' $ resty -e 'local a = {100, 200, 300, 400} for i = 1, 2 do print(a[1], a[2], a[3], a[4]) end'

Mari kita gali lebih dalam tentang unpack, dan kali ini kita dapat menggunakan restydoc untuk mencari.

$ restydoc -s unpack

Seperti yang Anda lihat dari dokumentasi unpack, unpack(list [, i [, j]]) setara dengan return list[i], list[i+1], list[j], dan Anda dapat menganggap unpack sebagai syntactic sugar. Dengan cara ini, Anda dapat mengaksesnya persis seperti indeks array tanpa mengganggu kompilasi JIT LuaJIT.

pairs()

Terakhir, mari kita lihat fungsi pairs() yang melintasi tabel hash, yang juga tidak dapat dikompilasi oleh JIT.

Namun, sayangnya, tidak ada alternatif yang setara untuk ini. Anda hanya dapat mencoba menghindarinya atau menggunakan array yang diakses oleh indeks numerik sebagai gantinya, dan khususnya, jangan melintasi tabel hash pada jalur kode panas. Di sini saya menjelaskan jalur kode panas, yang berarti kode akan dieksekusi berkali-kali, misalnya, di dalam loop besar.

Setelah membahas empat contoh ini, mari kita simpulkan bahwa untuk menghindari penggunaan fungsi NYI, Anda perlu memperhatikan dua poin ini.

  • Gunakan API yang disediakan oleh OpenResty daripada fungsi pustaka standar Lua. Ingatlah bahwa Lua adalah bahasa yang disematkan, dan kita memprogram di OpenResty, bukan Lua.
  • Jika Anda terpaksa menggunakan bahasa NYI sebagai upaya terakhir, pastikan itu tidak berada pada jalur kode panas.

Bagaimana mendeteksi NYI?

Semua pembahasan tentang penghindaran NYI ini adalah untuk mengajarkan Anda apa yang harus dilakukan. Namun, akan tidak konsisten dengan salah satu filosofi yang dianut OpenResty jika berakhir tiba-tiba di sini.

Apa yang dapat dilakukan secara otomatis oleh mesin tidak melibatkan manusia.

Manusia bukan mesin, dan akan selalu ada kelalaian. Mendeteksi NYI yang digunakan dalam kode secara otomatis adalah refleksi penting dari nilai seorang insinyur.

Di sini saya merekomendasikan modul jit.dump dan jit.v yang disertakan dengan LuaJIT. Keduanya mencetak proses bagaimana kompiler JIT bekerja. Yang pertama mengeluarkan informasi detail yang dapat digunakan untuk men-debug LuaJIT itu sendiri. Anda dapat merujuk ke kode sumbernya untuk pemahaman yang lebih dalam; yang kedua mengeluarkan output yang lebih sederhana, dengan setiap baris sesuai dengan trace, dan biasanya digunakan untuk memeriksa apakah dapat di-JIT.

Bagaimana kita melakukannya? Kita dapat memulai dengan menambahkan dua baris kode berikut ke init_by_lua.

local v = require "jit.v" v.on("/tmp/jit.log")

Kemudian, jalankan alat pengujian stres Anda atau beberapa ratus set pengujian unit untuk membuat LuaJIT cukup panas untuk memicu kompilasi JIT. Setelah itu, periksa hasil /tmp/jit.log.

Tentu saja, pendekatan ini relatif membosankan, jadi jika Anda ingin tetap sederhana, resty sudah cukup, dan CLI OpenResty dilengkapi dengan opsi berikut.

$resty -j v -e 'for i=1, 1000 do local newstr, n, err = ngx.re.gsub("hello, world", "([a-z])[a-z]+", "[$0,$1]", "i") end' [TRACE 1 (command line -e):1 stitch C:107bc91fd] [TRACE 2 (1/stitch) (command line -e):2 -> 1]

Di mana -j di resty adalah opsi terkait LuaJIT, nilai dump dan v mengikuti, sesuai dengan mengaktifkan mode jit.dump dan jit.v.

Dalam output modul jit.v, setiap baris adalah objek trace yang berhasil dikompilasi. Baru saja adalah contoh trace yang dapat di-JIT, dan jika fungsi NYI ditemui, output akan menentukan bahwa itu adalah NYI, seperti dalam contoh pairs berikut.

$resty -j v -e 'local t = {} for i=1,100 do t[i] = i end for i=1, 1000 do for j=1,1000 do for k,v in pairs(t) do -- end end end'

Tidak dapat di-JIT, sehingga hasilnya menunjukkan fungsi NYI di baris 8.

[TRACE 1 (command line -e):2 loop] [TRACE --- (command line -e):7 -- NYI: bytecode 72 at (command line -e):8]

Tulis di akhir

Ini adalah pertama kalinya kita membahas masalah performa OpenResty secara lebih panjang. Setelah membaca optimasi tentang NYI ini, apa pendapat Anda? Anda dapat meninggalkan komentar dengan pendapat Anda.

Terakhir, saya akan meninggalkan Anda dengan pertanyaan yang merenungkan ketika membahas alternatif untuk fungsi string.find(); saya menyebutkan bahwa akan lebih baik untuk melakukan lapisan pembungkus dan mengaktifkan opsi optimasi secara default. Jadi, saya akan meninggalkan tugas itu kepada Anda untuk sedikit uji coba.

Silakan tulis jawaban Anda di bagian komentar, dan Anda dipersilakan untuk membagikan artikel ini dengan rekan dan teman Anda untuk berkomunikasi dan berkembang bersama.