Keuntungan dan Kerugian `string` di OpenResty
API7.ai
December 8, 2022
Pada artikel sebelumnya, kita telah mengenal fungsi-fungsi blocking umum di OpenResty, yang sering disalahgunakan oleh pemula. Mulai dari artikel ini, kita akan masuk ke inti dari optimasi performa, yang akan melibatkan banyak teknik optimasi yang dapat membantu kita dengan cepat meningkatkan performa kode OpenResty, jadi jangan anggap remeh.
Dalam proses ini, kita perlu menulis lebih banyak kode pengujian untuk merasakan bagaimana menggunakan teknik-teknik optimasi ini dan memverifikasi efektivitasnya sehingga kita dapat memanfaatkannya dengan baik.
Di Balik Layar Tips Optimasi Performa
Teknik optimasi adalah bagian dari "praktik", jadi sebelum kita melakukannya, mari kita bahas "teori" dari optimasi.
Metode optimasi performa akan berubah seiring dengan iterasi LuaJIT dan OpenResty. Beberapa metode mungkin akan dioptimalkan langsung oleh teknologi dasar dan tidak perlu lagi dikuasai; pada saat yang sama, akan ada beberapa teknik optimasi baru. Oleh karena itu, yang paling penting adalah menguasai konsep konstan di balik teknik-teknik optimasi ini.
Mari kita lihat beberapa ide kritis tentang performa dalam pemrograman OpenResty.
Teori 1: Memproses permintaan harus singkat, sederhana, dan cepat
OpenResty adalah server web, sehingga sering menangani 1.000+, 10.000+, atau bahkan 100.000+ permintaan klien secara bersamaan. Oleh karena itu, untuk mencapai performa keseluruhan tertinggi, kita harus memastikan bahwa permintaan individu diproses dengan cepat dan berbagai sumber daya, seperti memori, dipulihkan.
- "Singkat" yang disebutkan di sini berarti siklus hidup permintaan harus singkat agar tidak mengambil sumber daya untuk waktu yang lama tanpa melepaskannya; bahkan untuk koneksi panjang, batas waktu atau jumlah permintaan harus ditetapkan untuk melepaskan sumber daya secara teratur.
- "Sederhana" yang kedua mengacu pada melakukan hanya satu hal dalam satu API. Pecahkan logika bisnis yang kompleks menjadi beberapa API dan pertahankan kode tetap sederhana.
- Terakhir, "cepat" berarti jangan memblokir thread utama dan jangan menjalankan terlalu banyak operasi CPU. Bahkan jika Anda harus melakukannya, jangan lupa untuk bekerja dengan metode lain yang kami perkenalkan di artikel sebelumnya.
Pertimbangan arsitektur ini tidak hanya cocok untuk OpenResty, tetapi juga untuk bahasa dan platform pengembangan lebih lanjut, jadi saya harap Anda dapat memahami dan memikirkannya dengan seksama.
Teori 2: Hindari menghasilkan data sementara
Menghindari data yang tidak berguna dalam proses sementara bisa dibilang adalah teori optimasi yang paling dominan dalam pemrograman OpenResty. Mari kita lihat contoh kecil untuk menjelaskan data yang tidak berguna dalam proses sementara.
$ resty -e 'local s= "hello" s = s .. " world" s = s .. "!" print(s) '
Dalam cuplikan kode ini, kita melakukan beberapa operasi penyambungan pada variabel s untuk mendapatkan hasil hello world!. Tetapi hanya keadaan akhir hello world! dari s yang berguna. Nilai awal s dan penugasan sementara adalah semua data sementara yang harus dihasilkan sesedikit mungkin.
Alasannya adalah bahwa data sementara ini akan membawa kerugian performa inisialisasi dan GC. Jangan meremehkan kerugian ini; jika ini muncul dalam kode panas seperti loop, performa akan jelas menurun. Saya juga akan menjelaskan ini nanti dengan contoh string.
string bersifat tidak berubah
Sekarang, kembali ke topik artikel ini, string. Di sini, saya menekankan fakta bahwa string bersifat tidak berubah di Lua.
Tentu saja, ini tidak berarti bahwa string tidak dapat disambung, dimodifikasi, dll., tetapi ketika kita memodifikasi string, kita tidak mengubah string asli tetapi membuat objek string baru dan mengubah referensi ke string. Jadi secara alami, jika string asli tidak memiliki referensi lain, itu akan dipulihkan oleh GC (garbage collection) Lua.
Manfaat yang jelas dari string yang tidak berubah adalah menghemat memori. Dengan cara ini, hanya akan ada satu salinan string yang sama dalam memori, dan variabel yang berbeda akan menunjuk ke alamat memori yang sama.
Kerugian dari desain ini adalah ketika datang ke penambahan dan pemulihan string, setiap kali Anda menambahkan string, LuaJIT harus memanggil lj_str_new untuk menanyakan apakah string sudah ada; jika tidak, perlu membuat string baru. Jika Anda melakukan ini sangat sering, itu akan memiliki dampak besar pada performa.
Mari kita lihat contoh konkret dari operasi penyambungan string seperti yang ada dalam contoh ini, yang ditemukan di banyak proyek open source OpenResty.
$ resty -e 'local begin = ngx.now() local s = "" -- `for` loop, menggunakan `..` untuk melakukan penyambungan string for i = 1, 100000 do s = s .. "a" end ngx.update_time() print(ngx.now() - begin) '
Apa yang dilakukan kode sampel ini adalah melakukan 100.000 penyambungan string pada variabel s dan mencetak waktu berjalan. Meskipun contoh ini agak ekstrem, ini memberikan gambaran yang baik tentang perbedaan sebelum dan setelah optimasi performa. Tanpa optimasi, kode ini berjalan selama 0,4 detik di laptop saya, yang masih relatif lambat. Jadi bagaimana kita harus mengoptimalkannya?
Dalam artikel sebelumnya, jawabannya diberikan, yaitu menggunakan table untuk melakukan lapisan enkapsulasi, menghapus semua string sementara dan hanya menyimpan data asli dan hasil akhir. Mari kita lihat implementasi kode konkretnya.
$ resty -e 'local begin = ngx.now() local t = {} -- for loop yang menggunakan array untuk menyimpan string, menghitung panjang array setiap kali for i = 1, 100000 do t[#t + 1] = "a" end -- Menyambung string menggunakan metode concat dari array local s = table.concat(t, "") ngx.update_time() print(ngx.now() - begin) '
Kita dapat melihat bahwa kode ini menyimpan setiap string secara bergantian dengan table, dan indeks ditentukan oleh #t + 1, yaitu panjang table saat ini ditambah 1. Terakhir, gunakan fungsi table.concat untuk menggabungkan setiap elemen array. Ini secara alami melewati semua string sementara dan menghindari 100.000 kali lj_str_new dan GC.
Itulah analisis kode kita, tetapi bagaimana hasil optimasinya? Kode yang dioptimalkan hanya membutuhkan 0,007 detik, yang berarti peningkatan performa lebih dari 50 kali. Dalam proyek nyata, peningkatan performa mungkin bahkan lebih terasa karena dalam contoh ini kita hanya menambahkan satu karakter a setiap kali.
Bagaimana perbedaan performa jika string baru memiliki panjang 10x a?
Apakah 0,007 detik kode sudah cukup baik untuk pekerjaan optimasi kita? Tidak, masih bisa dioptimalkan. Mari kita ubah satu baris kode lagi dan lihat hasilnya.
$ resty -e 'local begin = ngx.now() local t = {} -- for loop, menggunakan array untuk menyimpan string, mempertahankan panjang array itu sendiri for i = 1, 100000 do t[i] = "a" end local s = table.concat(t, "") ngx.update_time() print(ngx.now() - begin) '
Kali ini, kita mengubah t[#t + 1] = "a" menjadi t[i] = "a", dan hanya dengan satu baris kode, kita dapat menghindari 100.000 panggilan fungsi untuk mendapatkan panjang array. Ingat operasi untuk mendapatkan panjang array yang kita sebutkan di bagian table sebelumnya? Ini memiliki kompleksitas waktu O(n), operasi yang relatif mahal. Jadi, di sini kita cukup mempertahankan indeks array kita sendiri untuk melewati operasi mendapatkan panjang array. Seperti kata pepatah, jika Anda tidak mampu mengacaukannya, Anda bisa menghindarinya.
Tentu saja, ini adalah cara penulisan yang lebih sederhana. Kode berikut menggambarkan lebih jelas bagaimana mempertahankan indeks array sendiri.
$ resty -e 'local begin = ngx.now() local t = {} local index = 1 for i = 1, 100000 do t[index] = "a" index = index + 1 end local s = table.concat(t, "") ngx.update_time() print(ngx.now() - begin) '
Mengurangi string sementara lainnya
Kesalahan yang baru saja kita bicarakan, string sementara yang disebabkan oleh penyambungan string, sangat jelas. Dengan beberapa pengingat dari kode sampel di atas, saya yakin kita tidak akan membuat kesalahan serupa lagi. Namun, ada beberapa string sementara yang lebih tersembunyi yang dihasilkan di OpenResty, yang jauh lebih sulit dideteksi. Misalnya, fungsi penanganan string yang akan kita bahas di bawah ini sering digunakan. Bisakah Anda bayangkan bahwa itu juga menghasilkan string sementara?
Seperti yang kita ketahui, fungsi string.sub mengambil bagian tertentu dari string. Seperti yang kita sebutkan sebelumnya, string di Lua tidak berubah, jadi mengambil string baru melibatkan lj_str_new dan operasi GC berikutnya.
resty -e 'print(string.sub("abcd", 1, 1))'
Fungsi dari kode di atas adalah mengambil karakter pertama dari string dan mencetaknya. Secara alami, ini pasti menghasilkan string sementara. Apakah ada cara yang lebih baik untuk mencapai efek yang sama?
resty -e 'print(string.char(string.byte("abcd")))'
Tentu saja. Melihat kode ini, kita pertama-tama menggunakan string.byte untuk mendapatkan kode numerik dari karakter pertama dan kemudian menggunakan string.char untuk mengubah angka menjadi karakter yang sesuai. Proses ini tidak menghasilkan string sementara apa pun. Oleh karena itu, paling efisien untuk menggunakan string.byte untuk melakukan pemindaian dan analisis terkait string.
Memanfaatkan dukungan SDK untuk tipe table
Setelah mempelajari cara mengurangi string sementara, apakah Anda ingin mencobanya? Kemudian, kita dapat mengambil hasil dari kode sampel di atas dan mengeluarkannya ke klien sebagai konten badan respons. Pada titik ini, Anda dapat berhenti sejenak dan mencoba menulis kode ini sendiri terlebih dahulu.
$ resty -e 'local begin = ngx.now() local t = {} local index = 1 for i = 1, 100000 do t[index] = "a" index = index + 1 end local response = table.concat(t, "") ngx.say(response) '
Jika Anda dapat menulis kode ini, Anda sudah lebih maju dari kebanyakan pengembang OpenResty. API Lua OpenResty sudah mempertimbangkan penggunaan table untuk penyambungan string, jadi dalam ngx.say, ngx.print, ngx.log, cosocket:send, dan API lain yang mungkin mengambil banyak string, ia menerima tidak hanya string sebagai parameter, tetapi juga menerima table sebagai parameter.
resty -e 'local begin = ngx.now() local t = {} local index = 1 for i = 1, 100000 do t[index] = "a" index = index + 1 end ngx.say(t) '
Dalam cuplikan kode terakhir ini, kita menghilangkan local response = table.concat(t, ""), langkah penyambungan string, dan langsung meneruskan table ke ngx.say. Ini menggeser tugas penyambungan string dari tingkat Lua ke tingkat C, menghindari pencarian, pembuatan, dan GC string lagi. Untuk string panjang, ini adalah peningkatan performa yang signifikan.
Ringkasan
Setelah membaca artikel ini, kita dapat melihat bahwa banyak optimasi performa OpenResty berurusan dengan berbagai detail. Oleh karena itu, kita perlu mengetahui LuaJIT dan API Lua OpenResty dengan baik untuk mencapai performa optimal. Ini juga mengingatkan kita bahwa jika kita telah melupakan konten sebelumnya, kita harus meninjau dan mengkonsolidasinya tepat waktu.
Terakhir, pikirkan masalah ini: tulis string hello, world, dan ! ke log kesalahan. Bisakah kita menulis kode sampel tanpa penyambungan string?
Juga, jangan lupa pertanyaan lain dalam teks. Bagaimana perbedaan performa dalam kode berikut jika string baru memiliki panjang 10x a?
$ resty -e 'local begin = ngx.now() local t = {} for i = 1, 100000 do t[#t + 1] = "a" end local s = table.concat(t, "") ngx.update_time() print(ngx.now() - begin) '
Anda juga dipersilakan untuk membagikan artikel ini dengan teman-teman Anda untuk belajar dan berkomunikasi.