Saya benci kode pembandingan, sama seperti manusia lainnya (yang, pada saat ini, sebagian besar pemirsanya mungkin bukan ¯_(ツ)_/¯). Jauh lebih menyenangkan untuk berpura-pura bahwa cache suatu nilai meningkatkan kinerja 1000% daripada menguji untuk melihat apa yang dilakukannya. Sayangnya, pembandingan dalam JavaScript masih diperlukan, terutama karena JavaScript digunakan (padahal seharusnya tidak?) dalam aplikasi yang lebih sensitif terhadap kinerja. Sayangnya, karena banyaknya keputusan arsitektur inti, JavaScript tidak membuat benchmarking menjadi lebih mudah.

Apa yang salah dengan JavaScript?

Kompiler JIT mengurangi akurasi (?)

Bagi mereka yang tidak terbiasa dengan keajaiban bahasa skrip modern seperti JavaScript, arsitekturnya bisa jadi sangat rumit. Daripada 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 terintegrasi beberapa tingkatan “kompiler”.

Masing-masing kompiler ini menawarkan trade-off yang berbeda antara waktu kompilasi dan kinerja waktu proses, sehingga pengguna tidak perlu menghabiskan kode pengoptimalan komputasi yang jarang dijalankan sambil memanfaatkan manfaat kinerja kompiler yang lebih canggih untuk kode yang paling sering dijalankan ( “jalur panas”). Ada juga beberapa komplikasi lain yang muncul saat menggunakan kompiler pengoptimalan yang melibatkan kata-kata pemrograman yang rumit seperti “monomorfisme fungsi”, tapi saya akan menghindarkan Anda dan menghindari membicarakan hal itu di sini.

Jadi… mengapa hal ini penting untuk benchmarking? Seperti yang sudah Anda duga, karena benchmarking adalah mengukur pertunjukan kode, kompiler JIT dapat memiliki pengaruh yang cukup besar. Potongan kode yang lebih kecil, ketika diukur, sering kali dapat melihat peningkatan kinerja 10x+ setelah pengoptimalan penuh, sehingga menimbulkan banyak kesalahan pada hasilnya. Misalnya, dalam pengaturan benchmarking paling dasar (Jangan gunakan hal seperti di bawah ini karena berbagai alasan):

for (int i = 0; i<1000; i++) {
    console.time()
    
    console.timeEnd()
}

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

Banyak kode Anda akan di-cache setelah beberapa kali percobaan, sehingga mengurangi waktu per operasi secara signifikan. Program benchmark sering kali melakukan yang terbaik untuk menghilangkan caching/optimasi ini, karena hal ini juga dapat membuat program yang diuji kemudian dalam proses benchmark tampil relatif lebih cepat. Namun, pada akhirnya Anda harus bertanya apakah tolok ukur tanpa pengoptimalan cocok dengan kinerja di dunia nyata. Tentu saja, dalam kasus tertentu, seperti halaman web yang jarang diakses, pengoptimalan tidak mungkin dilakukan, namun dalam lingkungan seperti server, di mana kinerja adalah yang paling penting, pengoptimalan harus diharapkan. Jika Anda menjalankan sepotong kode sebagai middleware untuk ribuan permintaan per detik, Anda sebaiknya berharap V8 dapat mengoptimalkannya.

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

Mesin melakukan yang terbaik untuk menghentikan Anda menentukan waktu secara akurat

Anda tahu sidik jari? Teknik yang memungkinkan Jangan Lacak agar terbiasa bantuan pelacakan? Ya, mesin JavaScript telah melakukan yang terbaik untuk memitigasinya. Upaya ini, dibarengi dengan langkah pencegahan serangan waktumenyebabkan mesin JavaScript dengan sengaja membuat pengaturan waktu menjadi tidak akurat, sehingga peretas tidak dapat memperoleh pengukuran yang tepat mengenai kinerja komputer saat ini atau seberapa mahal biaya operasi tertentu. Sayangnya, ini berarti, tanpa melakukan penyesuaian, benchmark akan mengalami masalah yang sama.

Contoh di bagian sebelumnya tidak akurat karena hanya diukur dalam hitungan milidetik. Sekarang, matikan itu performance.now(). Hebat, Sekarang kita punya stempel waktu dalam mikrodetik!


console.time();

console.timeEnd();


const t = performance.now();

console.log(performance.now() - t);

Kecuali… semuanya dalam kelipatan 100μs. Sekarang mari kita buat tambahkan beberapa header untuk mengurangi risiko serangan waktu. Ups, kami hanya dapat menambah 5μs. 5μs mungkin merupakan presisi yang cukup untuk banyak kasus penggunaan, namun Anda harus mencari di tempat lain untuk sesuatu yang memerlukan lebih banyak granularitas. Sejauh yang saya tahu, tidak ada browser yang mengizinkan pengatur waktu yang lebih terperinci. Node.js melakukannya, tetapi tentu saja, hal itu memiliki masalah tersendiri.

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

Setiap lingkungan berbeda

saya suka Bukan karena apa yang telah dilakukannya untuk mendorong JavaScript sisi server maju, tapi sialnya, itu membuat pembandingan JavaScript untuk server jauh lebih sulit. Beberapa tahun yang lalu, satu-satunya lingkungan JavaScript sisi server yang dipedulikan orang adalah Node.js dan Deno, keduanya menggunakan mesin JavaScript V8 (yang sama di Chrome). Bun malah menggunakan JavaScriptCore, mesin di Safari, yang memiliki karakteristik kinerja yang sangat berbeda.

Masalah beberapa lingkungan JavaScript dengan karakteristik kinerjanya sendiri ini relatif baru di JavaScript sisi server tetapi telah menjangkiti klien sejak lama. 3 mesin JavaScript berbeda yang umum digunakan, V8, JSC, dan SpiderMonkey untuk Chrome, Safari, dan Firefox, semuanya dapat bekerja jauh lebih cepat atau lebih lambat pada potongan kode yang setara.

Salah satu contoh perbedaan tersebut adalah pada Tail Call Optimization (TCO). TCO mengoptimalkan fungsi yang berulang di akhir tubuhnya, seperti ini:

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

Cobalah membuat tolok ukur factorial(100000) di Bun. Sekarang, coba hal yang sama di Node.js atau Deno. Anda akan mendapatkan kesalahan 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 benar-benar baru untuk menjalankan fungsi bersarang, yang pada akhirnya dibatasi oleh tumpukan panggilan. Tapi kenapa hal 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 memiliki tolok ukur yang 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 untuk memastikan kode yang cepat di satu mesin tidak lambat di mesin lainnya. Selain itu, jika Anda mengembangkan perpustakaan yang Anda harapkan dapat digunakan di banyak platform, pastikan untuk menyertakan lebih banyak mesin esoterik seperti Hermes; mereka memiliki karakteristik kinerja yang sangat berbeda.

Sebutan yang terhormat

  • Pengumpul sampah dan kecenderungannya untuk menghentikan semuanya secara acak
  • Kemampuan kompiler JIT untuk menghapus semua kode Anda karena “tidak perlu”
  • Grafik api yang sangat luas di sebagian besar alat pengembang JavaScript
  • Saya pikir Anda mengerti maksudnya

Jadi… apa solusinya?

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

Di server, Anda memiliki waktu yang sedikit lebih mudah. Anda dapat menggunakan d8 untuk mengontrol tingkat pengoptimalan secara manual, mengontrol pengumpul sampah, dan mendapatkan waktu yang tepat. Tentu saja, Anda memerlukan beberapa Bash-fu untuk menyiapkan pipeline benchmark yang dirancang dengan baik untuk ini, karena sayangnya d8 tidak terintegrasi dengan baik (atau terintegrasi sama sekali) dengan Node.js. Anda juga dapat mengaktifkan tanda tertentu di Node.js untuk mendapatkan hasil serupa, namun Anda akan kehilangan fitur seperti mengaktifkan tingkat pengoptimalan tertentu.

v8 --sparkplug --always-sparkplug --no-opt (file)

Contoh D8 dengan tingkat kompilasi tertentu (sparkplug) diaktifkan. D8, secara default, menyertakan lebih banyak kontrol atas GC dan lebih banyak info debug secara umum.

Anda bisa mendapatkan beberapa fitur serupa di JavaScriptCore??? Sejujurnya, saya belum banyak menggunakan CLI JavaScriptCore, dan memang begitu berat kurang terdokumentasi. Anda dapat mengaktifkan tingkatan tertentu menggunakan bendera baris perintah merekatapi saya tidak yakin berapa banyak informasi debug yang dapat Anda ambil. Bun juga menyertakan beberapa hal yang bermanfaat utilitas pembandingantetapi terbatasnya sama seperti Node.js.

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

Jika Anda hanya mencoba melakukan benchmark pada mesin dengan opsi default seakurat mungkin di server, ada alat Node.js yang tersedia seperti ukuran 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 pipeline seperti di atas.

Di browser, semuanya jauh lebih rumit. Saya tidak tahu solusi apa pun untuk pengaturan waktu yang lebih tepat, dan kendali mesin jauh lebih terbatas. Informasi terbanyak yang bisa Anda peroleh terkait kinerja JavaScript runtime di browser berasal dari Alat pengembang Chromeyang menawarkan grafik api dasar dan utilitas simulasi perlambatan CPU.

Kesimpulan

Banyak keputusan desain yang membuat JavaScript (relatif) berperforma tinggi dan portabel menjadikan pembandingan jauh lebih sulit dibandingkan dengan bahasa lain. Ada lebih banyak target yang harus dijadikan patokan, dan Anda memiliki lebih sedikit kendali pada setiap target.

Mudah-mudahan, suatu solusi suatu hari nanti akan menyederhanakan banyak masalah ini. Saya mungkin pada akhirnya akan membuat alat untuk menyederhanakan benchmarking lintas mesin dan tingkat kompilasi, namun untuk saat ini, membuat pipeline untuk menyelesaikan semua masalah ini memerlukan sedikit usaha. 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 benchmark pada lingkungan lain.

Bagaimanapun Anda memilih untuk melakukan benchmark, saya harap artikel ini menunjukkan kepada Anda beberapa masalah yang ada dalam benchmarking JavaScript. Beri tahu saya jika tutorial penerapan beberapa hal yang saya jelaskan di atas dapat membantu.

Sumber

Krystian Wiśniewski
Krystian Wiśniewski is a dedicated Sports Reporter and Editor with a degree in Sports Journalism from He graduated with a degree in Journalism from the University of Warsaw. Bringing over 14 years of international reporting experience, Krystian has covered major sports events across Europe, Asia, and the United States of America. Known for his dynamic storytelling and in-depth analysis, he is passionate about capturing the excitement of sports for global audiences and currently leads sports coverage and editorial projects at Agen BRILink dan BRI.