paint-brush
Mengapa Pembandingan JavaScript Berantakan?oleh@asyncbanana
Sejarah baru

Mengapa Pembandingan JavaScript Berantakan?

oleh AsyncBanana7m2025/01/04
Read on Terminal Reader

Terlalu panjang; Untuk membaca

Pembandingan JavaScript masih diperlukan, terutama karena JavaScript digunakan dalam aplikasi yang lebih sensitif terhadap kinerja. Mesin JavaScript telah melakukan yang terbaik untuk mengurangi serangan pengaturan waktu, bersamaan dengan langkah untuk beralih ke [Jangan Lacak]. Mesin JavaScript sengaja membuat pengukuran pengaturan waktu menjadi tidak akurat.
featured image - Mengapa Pembandingan JavaScript Berantakan?
AsyncBanana HackerNoon profile picture
0-item
1-item

Saya benci kode benchmarking, sama seperti manusia lainnya (yang, pada titik ini, sebagian besar pemirsa ini mungkin tidak ¯\ (ツ) /¯). Jauh lebih menyenangkan untuk berpura-pura bahwa caching suatu nilai meningkatkan kinerja sebesar 1000% daripada menguji untuk melihat apa yang terjadi. Sayangnya, benchmarking dalam JavaScript masih diperlukan, terutama karena JavaScript digunakan ( padahal seharusnya tidak? ) dalam aplikasi yang lebih sensitif terhadap kinerja. Sayangnya, karena banyak keputusan arsitektur intinya, JavaScript tidak membuat benchmarking menjadi lebih mudah.

Apa yang Salah dengan JavaScript?

Kompiler JIT Meningkatkan Akurasi(?)

Bagi mereka yang tidak terbiasa dengan keajaiban bahasa skrip modern seperti JavaScript, arsitekturnya bisa jadi cukup rumit. Alih-alih hanya menjalankan kode melalui penerjemah yang langsung mengeluarkan instruksi, sebagian besar mesin JavaScript menggunakan arsitektur yang lebih mirip dengan bahasa yang dikompilasi seperti C—mereka mengintegrasikan beberapa tingkatan "kompiler" .


Masing-masing kompiler ini menawarkan tradeoff yang berbeda antara waktu kompilasi dan kinerja waktu proses, sehingga pengguna tidak perlu menghabiskan komputasi untuk mengoptimalkan kode yang jarang dijalankan sambil memanfaatkan manfaat kinerja kompiler yang lebih canggih untuk kode yang paling sering dijalankan ("hot path"). Ada juga beberapa komplikasi lain yang muncul saat menggunakan kompiler pengoptimalan yang melibatkan kata-kata pemrograman yang rumit seperti " fungsi monomorfisme ", tetapi saya akan menghindarkan Anda dan menghindari membicarakannya di sini.


Jadi... mengapa ini penting untuk benchmarking? Nah, seperti yang mungkin sudah Anda duga, karena benchmarking mengukur kinerja kode, kompiler JIT dapat memiliki pengaruh yang cukup besar. Potongan kode yang lebih kecil, ketika di-benchmark, sering kali dapat melihat peningkatan kinerja 10x+ setelah pengoptimalan penuh, yang menghasilkan banyak kesalahan dalam hasil.


Misalnya, dalam pengaturan pembandingan paling dasar (Jangan gunakan apa pun seperti di bawah ini karena beberapa alasan):

 for (int i = 0; i<1000; i++) { console.time() // do some expensive work console.timeEnd() }

(Jangan khawatir, kita akan membicarakan console.time juga)


Banyak kode Anda akan di-cache setelah beberapa kali percobaan, yang akan mengurangi waktu per operasi secara signifikan. Program benchmark sering kali berupaya sebaik mungkin untuk menghilangkan caching/optimasi ini, karena hal ini juga dapat membuat program yang diuji kemudian dalam proses benchmark tampak relatif lebih cepat. Namun, pada akhirnya Anda harus bertanya apakah benchmark tanpa optimasi sesuai dengan kinerja di dunia nyata.


Tentu saja, dalam kasus tertentu, seperti halaman web yang jarang diakses, pengoptimalan tidak mungkin dilakukan, tetapi dalam lingkungan seperti server, di mana kinerja adalah yang terpenting, pengoptimalan harus diharapkan. Jika Anda menjalankan sepotong kode sebagai middleware untuk ribuan permintaan per detik, Anda sebaiknya berharap V8 mengoptimalkannya.


Jadi pada dasarnya, bahkan dalam satu mesin, ada 2-4 cara berbeda untuk menjalankan kode Anda dengan berbagai tingkat kinerja yang berbeda. Oh, juga, sangat sulit dalam kasus tertentu untuk memastikan tingkat pengoptimalan tertentu diaktifkan. Selamat bersenang-senang :).

Mesin berusaha sebaik mungkin untuk mencegah Anda menentukan waktu secara akurat

Tahukah Anda tentang sidik jari? Teknik yang memungkinkan Do Not Track digunakan untuk membantu pelacakan ? Ya, mesin JavaScript telah melakukan yang terbaik untuk mengatasinya. Upaya ini, bersama dengan langkah untuk mencegah serangan pengaturan waktu , menyebabkan mesin JavaScript sengaja membuat pengaturan waktu tidak akurat, sehingga peretas tidak bisa mendapatkan pengukuran yang tepat dari kinerja komputer saat ini atau seberapa mahal biaya operasi tertentu.


Sayangnya, ini berarti bahwa tanpa melakukan penyesuaian, benchmark akan memiliki masalah yang sama.


Contoh di bagian sebelumnya tidak akan akurat, karena hanya diukur dalam milidetik. Sekarang, ganti dengan performance.now() . Bagus.


Sekarang, kita memiliki stempel waktu dalam mikrodetik!

 // Bad console.time(); // work console.timeEnd(); // Better? const t = performance.now(); // work console.log(performance.now() - t);

Kecuali... semuanya dalam kelipatan 100μs. Sekarang, mari tambahkan beberapa header untuk mengurangi risiko serangan pengaturan waktu. Ups, kita masih hanya bisa menambahkan kelipatan 5μs. 5μs mungkin cukup presisi untuk banyak kasus penggunaan, tetapi Anda harus mencari di tempat lain untuk apa pun yang memerlukan ketelitian lebih. Sejauh yang saya ketahui, tidak ada browser yang memungkinkan pengatur waktu yang lebih terperinci. Node.js memungkinkan, tetapi tentu saja, itu punya masalahnya sendiri.


Bahkan jika Anda memutuskan untuk menjalankan kode melalui browser dan membiarkan kompiler melakukan tugasnya, jelas, Anda akan tetap mengalami lebih banyak masalah jika menginginkan pengaturan waktu yang akurat. Oh ya, dan tidak semua browser dibuat sama.

Setiap Lingkungan Berbeda

Saya suka Bun karena apa yang telah dilakukannya untuk mendorong JavaScript sisi server ke depan, tetapi sialnya, Bun membuat pembandingan JavaScript untuk server menjadi jauh lebih sulit. Beberapa tahun yang lalu, satu-satunya lingkungan JavaScript sisi server yang diperhatikan orang adalah Node.js dan Deno , yang keduanya menggunakan mesin JavaScript V8 (yang sama di Chrome). Bun malah menggunakan JavaScriptCore, mesin di Safari, yang memiliki karakteristik kinerja yang sama sekali berbeda.


Masalah lingkungan JavaScript ganda dengan karakteristik kinerjanya sendiri ini tergolong baru dalam JavaScript sisi server, tetapi telah lama mengganggu klien. Ketiga mesin JavaScript yang umum digunakan, V8, JSC, dan SpiderMonkey untuk Chrome, Safari, dan Firefox, masing-masing, semuanya dapat bekerja secara signifikan lebih cepat atau lebih lambat pada bagian kode yang setara.


Salah satu contoh perbedaan ini adalah dalam Tail Call Optimization (TCO). TCO mengoptimalkan fungsi yang berulang di bagian akhir badannya, seperti ini:

 function factorial(i, num = 1) { if (i == 1) return num; num *= i; i--; return factorial(i, num); }


Coba lakukan benchmarking factorial(100000) di Bun. Sekarang, coba hal yang sama di Node.js atau Deno. Anda akan mendapatkan error yang mirip dengan ini:

 function factorial(i, num = 1) { ^ RangeError: Maximum call stack size exceeded


Di V8 (dan dengan ekstensi Node.js dan Deno), setiap kali factorial() memanggil dirinya sendiri di akhir, mesin membuat konteks fungsi yang sama sekali baru untuk menjalankan fungsi bersarang, yang akhirnya dibatasi oleh tumpukan panggilan. Namun mengapa ini tidak terjadi di Bun? JavaScriptCore, yang digunakan Bun, mengimplementasikan TCO, yang mengoptimalkan jenis fungsi ini dengan mengubahnya menjadi for loop seperti ini:

 function factorial(i, num = 1) { while (i != 1) { num *= i; i--; } return i; }

Desain di atas tidak hanya menghindari batasan tumpukan panggilan, tetapi juga jauh lebih cepat karena tidak memerlukan konteks fungsi baru, yang berarti fungsi seperti di atas akan diuji secara sangat berbeda pada mesin yang berbeda.


Pada dasarnya, perbedaan ini berarti Anda harus melakukan benchmark pada semua mesin yang Anda harapkan untuk menjalankan kode Anda guna memastikan kode yang cepat di satu mesin tidak lambat di mesin lain. Selain itu, jika Anda mengembangkan pustaka yang Anda harapkan untuk digunakan di banyak platform, pastikan untuk menyertakan mesin yang lebih esoteris seperti Hermes ; keduanya memiliki karakteristik kinerja yang sangat berbeda.

Penghargaan Terhormat

  • Pengumpul sampah dan kecenderungannya untuk menghentikan semuanya secara acak.


  • Kemampuan kompiler JIT untuk menghapus semua kode Anda karena “tidak diperlukan”.


  • Grafik api yang sangat lebar di sebagian besar alat pengembangan JavaScript.


  • Saya pikir Anda mengerti maksudnya.

Jadi… Apa Solusinya?

Saya berharap dapat menunjukkan paket npm yang dapat menyelesaikan semua masalah ini, tetapi kenyataannya tidak ada satu pun.


Di server, Anda akan sedikit lebih mudah. Anda dapat menggunakan d8 untuk mengontrol level optimasi secara manual, mengontrol pemungut sampah, dan mendapatkan pengaturan waktu yang tepat. Tentu saja, Anda akan memerlukan Bash-fu untuk menyiapkan alur kerja benchmark yang dirancang dengan baik untuk ini, karena sayangnya, d8 tidak terintegrasi dengan baik (atau tidak terintegrasi sama sekali) dengan Node.js.


Anda juga dapat mengaktifkan tanda tertentu di Node.js untuk mendapatkan hasil serupa, tetapi Anda akan kehilangan fitur seperti mengaktifkan tingkatan pengoptimalan tertentu.

 v8 --sparkplug --always-sparkplug --no-opt [file]

Contoh D8 dengan tingkatan kompilasi tertentu (sparkplug) yang diaktifkan. D8, secara default, mencakup lebih banyak kontrol GC dan lebih banyak info debug secara umum.


Anda bisa mendapatkan beberapa fitur serupa di JavaScriptCore??? Sejujurnya, saya belum banyak menggunakan CLI JavaScriptCore, dan sangat kurang terdokumentasi. Anda dapat mengaktifkan tingkatan tertentu menggunakan tanda baris perintahnya , tetapi saya tidak yakin berapa banyak informasi debug yang dapat Anda peroleh. Bun juga menyertakan beberapa utilitas pembandingan yang bermanfaat, tetapi semuanya terbatas seperti Node.js.


Sayangnya, semua ini memerlukan mesin dasar/versi uji dari mesin tersebut, yang mungkin cukup sulit didapatkan. Saya telah menemukan bahwa cara paling sederhana untuk mengelola mesin adalah dengan memasangkan esvu dengan eshost-cli , karena keduanya membuat pengelolaan mesin dan menjalankan kode di antara keduanya menjadi jauh lebih mudah. Tentu saja, masih banyak pekerjaan manual yang diperlukan, karena alat-alat ini hanya mengelola kode yang berjalan di antara berbagai mesin—Anda masih harus menulis sendiri kode pembandingan.


Jika Anda hanya mencoba melakukan benchmark pada mesin dengan opsi default seakurat mungkin di server, ada alat Node.js siap pakai seperti mitata yang membantu meningkatkan akurasi waktu dan kesalahan terkait GC. Banyak dari alat ini, seperti Mitata, juga dapat digunakan di banyak mesin; tentu saja, Anda masih harus menyiapkan alur kerja seperti di atas.


Di browser, semuanya jauh lebih sulit. Saya tidak tahu solusi apa pun untuk pengaturan waktu yang lebih tepat, dan kontrol mesin jauh lebih terbatas. Informasi terbanyak yang bisa Anda dapatkan terkait kinerja JavaScript saat dijalankan di browser akan berasal dari devtools Chrome , yang menawarkan utilitas simulasi grafik nyala api dan perlambatan CPU dasar.

Kesimpulan

Banyak keputusan desain yang sama yang membuat JavaScript (relatif) berkinerja dan portabel membuat pembandingan jauh lebih sulit daripada dalam bahasa lain. Ada lebih banyak target untuk pembandingan, dan Anda memiliki kontrol yang jauh lebih sedikit atas setiap target.


Mudah-mudahan, suatu hari nanti ada solusi yang dapat menyederhanakan banyak masalah ini. Saya mungkin akhirnya membuat alat untuk menyederhanakan pembandingan lintas mesin dan tingkat kompilasi, tetapi untuk saat ini, membuat alur kerja untuk menyelesaikan semua masalah ini memerlukan banyak pekerjaan. Tentu saja, penting untuk diingat bahwa masalah ini tidak berlaku untuk semua orang—jika kode Anda hanya berjalan di satu lingkungan, jangan buang waktu Anda untuk melakukan pembandingan di lingkungan lain.


Apa pun cara yang Anda pilih untuk melakukan benchmark, saya harap artikel ini menunjukkan beberapa masalah yang ada dalam benchmark JavaScript. Beri tahu saya jika tutorial tentang penerapan beberapa hal yang saya jelaskan di atas dapat membantu.