Saya membaca slide Melanggar Batas Kecepatan Javascript dengan V8 , dan ada contoh seperti kode di bawah ini. Saya tidak tahu mengapa <=
lebih lambat daripada <
dalam kasus ini, adakah yang bisa menjelaskan hal itu? Setiap komentar dihargai.
Lambat:
this.isPrimeDivisible = function(candidate) {
for (var i = 1; i <= this.prime_count; ++i) {
if (candidate % this.primes[i] == 0) return true;
}
return false;
}
(Petunjuk: primes adalah array dengan panjang prime_count)
Lebih cepat:
this.isPrimeDivisible = function(candidate) {
for (var i = 1; i < this.prime_count; ++i) {
if (candidate % this.primes[i] == 0) return true;
}
return false;
}
[Info Lebih Lanjut] peningkatan kecepatannya signifikan, dalam uji lingkungan lokal saya, hasilnya adalah sebagai berikut:
V8 version 7.3.0 (candidate)
Lambat:
time d8 prime.js
287107
12.71 user
0.05 system
0:12.84 elapsed
Lebih cepat:
time d8 prime.js
287107
1.82 user
0.01 system
0:01.84 elapsed
javascript
v8
Leonardo Physh
sumber
sumber
<=
dan<
identik, baik dalam teori maupun dalam implementasi aktual di semua prosesor modern (dan juru bahasa).main
kode yang memanggil fungsi itu dalam satu loop yang berjalan25000
kali, jadi Anda melakukan banyak iterasi yang lebih sedikit secara keseluruhan melakukan perubahan itu. Juga, jika sebuah array memiliki panjang 5, mencoba untuk memperoleharray[5]
akan melampaui batasnya memberikanundefined
nilai sejak array mulai mengindeks0
.<=
tetapi bertindak identik dengan<
versi dengan melakukani <= this.prime_count - 1
. Ini memecahkan masalah "iterasi ekstra" dan masalah "yang melewati akhir array".Jawaban:
Saya bekerja di V8 di Google, dan ingin memberikan beberapa wawasan tambahan di atas jawaban dan komentar yang ada.
Untuk referensi, inilah contoh kode lengkap dari slide :
Pertama dan terpenting, perbedaan kinerja tidak ada hubungannya dengan operator
<
dan<=
secara langsung. Jadi tolong jangan melompat melalui lingkaran hanya untuk menghindari<=
dalam kode Anda karena Anda membaca di Stack Overflow bahwa itu lambat --- tidak!Kedua, orang-orang menunjukkan bahwa array itu "berlubang". Ini tidak jelas dari cuplikan kode di pos OP, tetapi jelas ketika Anda melihat kode yang menginisialisasi
this.primes
:Hasil ini dalam sebuah array dengan sebuah
HOLEY
elemen jenis di V8, bahkan jika array berakhir sepenuhnya diisi / dikemas / berdekatan. Secara umum, operasi pada array berlubang lebih lambat daripada operasi pada array dikemas, tetapi dalam kasus ini perbedaannya dapat diabaikan: itu berjumlah 1 tambahan Smi ( bilangan bulat kecil ) cek (untuk menjaga terhadap lubang) setiap kali kita menekanthis.primes[i]
dalam loopisPrimeDivisible
. Bukan masalah besar!TL; DR Makhluk array
HOLEY
bukan masalah di sini.Lainnya menunjukkan bahwa kode tersebut dibaca di luar batas. Ini umumnya direkomendasikan untuk menghindari membaca melampaui panjang array , dan dalam hal ini memang akan menghindari penurunan kinerja yang besar. Tapi mengapa? V8 dapat menangani beberapa skenario tidak terikat ini hanya dengan dampak kinerja kecil. Apa yang istimewa dari kasus khusus ini?
The out-of-bounds membaca hasil
this.primes[i]
beradaundefined
di baris ini:Dan itu membawa kita ke masalah sebenarnya :
%
operator sekarang sedang digunakan dengan operan non-integer!integer % someOtherInteger
dapat dihitung dengan sangat efisien; Mesin JavaScript dapat menghasilkan kode mesin yang sangat dioptimalkan untuk kasus ini.integer % undefined
di sisi lain jumlah cara yang kurang efisienFloat64Mod
, karenaundefined
direpresentasikan sebagai ganda.Cuplikan kode memang dapat ditingkatkan dengan mengubah
<=
menjadi<
pada baris ini:... bukan karena
<=
entah bagaimana operator yang unggul daripada<
, tetapi hanya karena ini menghindari out-of-bounds yang dibaca dalam kasus khusus ini.sumber
Jawaban dan komentar lain menyebutkan bahwa perbedaan antara dua loop adalah bahwa yang pertama mengeksekusi satu iterasi lebih dari yang kedua. Ini benar, tetapi dalam array yang tumbuh hingga 25.000 elemen, satu iterasi lebih atau kurang hanya akan membuat perbedaan kecil. Sebagai perkiraan kasarnya, jika kita mengasumsikan panjang rata-rata saat tumbuh adalah 12.500, maka perbedaan yang kita harapkan seharusnya sekitar 1 / 12.500, atau hanya 0,008%.
Perbedaan kinerja di sini jauh lebih besar daripada yang akan dijelaskan oleh satu iterasi tambahan, dan masalahnya dijelaskan pada akhir presentasi.
this.primes
adalah array yang bersebelahan (setiap elemen memiliki nilai) dan elemen adalah semua angka.Mesin JavaScript dapat mengoptimalkan array seperti itu menjadi array sederhana dari angka aktual, bukan array objek yang kebetulan mengandung angka tetapi bisa mengandung nilai lain atau tidak ada nilai. Format pertama jauh lebih cepat untuk diakses: dibutuhkan lebih sedikit kode, dan array jauh lebih kecil sehingga lebih sesuai dalam cache. Tetapi ada beberapa kondisi yang dapat mencegah penggunaan format yang dioptimalkan ini.
Salah satu syaratnya adalah jika beberapa elemen array hilang. Sebagai contoh:
Sekarang apa nilainya
a[1]
? Tidak memiliki nilai . (Bahkan tidak benar untuk mengatakan memiliki nilaiundefined
- elemen array yang mengandungundefined
nilai berbeda dari elemen array yang hilang seluruhnya.)Tidak ada cara untuk merepresentasikan ini hanya dengan angka, jadi mesin JavaScript terpaksa menggunakan format yang kurang dioptimalkan. Jika
a[1]
berisi nilai numerik seperti dua elemen lainnya, array berpotensi dapat dioptimalkan menjadi array angka saja.Alasan lain untuk array yang akan dipaksa ke dalam format deoptimisasi bisa jika Anda mencoba mengakses elemen di luar batas array, seperti yang dibahas dalam presentasi.
Loop pertama dengan
<=
upaya untuk membaca elemen melewati akhir array. Algoritma masih bekerja dengan benar, karena dalam iterasi tambahan terakhir:this.primes[i]
mengevaluasiundefined
karenai
melewati akhir array.candidate % undefined
(untuk nilai apa puncandidate
) dievaluasi menjadiNaN
.NaN == 0
mengevaluasi kefalse
.return true
tidak dieksekusi.Jadi seolah-olah iterasi ekstra tidak pernah terjadi - itu tidak berpengaruh pada sisa logika. Kode menghasilkan hasil yang sama seperti tanpa iterasi tambahan.
Tetapi untuk sampai ke sana, ia mencoba membaca elemen yang tidak ada melewati akhir array. Ini memaksa array keluar dari optimasi - atau setidaknya lakukan pada saat pembicaraan ini.
Loop kedua dengan
<
hanya membaca elemen yang ada dalam array, sehingga memungkinkan array dan kode yang dioptimalkan.Masalahnya dijelaskan di halaman 90-91 dari pembicaraan, dengan diskusi terkait di halaman sebelum dan sesudah itu.
Saya kebetulan menghadiri presentasi Google I / O yang sangat ini dan berbicara dengan pembicara (salah satu penulis V8) sesudahnya. Saya telah menggunakan teknik dalam kode saya sendiri yang melibatkan membaca melewati akhir array sebagai upaya yang salah arah (di belakang) untuk mengoptimalkan satu situasi tertentu. Dia mengonfirmasi bahwa jika Anda mencoba untuk bahkan membaca melewati akhir array, itu akan mencegah format sederhana yang dioptimalkan digunakan.
Jika apa yang dikatakan penulis V8 masih benar, maka membaca melewati akhir array akan mencegahnya dioptimalkan dan harus kembali ke format yang lebih lambat.
Sekarang mungkin V8 telah diperbaiki sementara itu untuk menangani kasus ini secara efisien, atau bahwa mesin JavaScript lainnya menanganinya secara berbeda. Saya tidak tahu satu atau lain cara dalam hal itu, tetapi deoptimisasi ini adalah apa yang dibicarakan presentasi.
sumber
undefined
bukan angka yang mengarah ke perhitungan yang berbeda.Object
), karena harus mendukung campuran tipe data apa pun dalam array. Seperti yang saya sebutkan di atas, kode dalam loop yang diumpankanundefined
tidak mempengaruhi kebenaran algoritma - itu tidak mengubah perhitungan sama sekali (seolah-olah iterasi tambahan tidak pernah terjadi).Value
objek yang dapat menyimpan referensi ke nilai-nilai dari jenis apa pun. (Saya mengarang namanyaValue
, tetapi intinya adalah bahwa elemen array bukan hanya angka sederhana tetapi juga objek yang membungkus angka atau tipe lainnya.)HOLEY
karena dibuat menggunakannew Array(n)
(meskipun bagian kode ini tidak terlihat di OP).HOLEY
array tetapHOLEY
selamanya di V8 , bahkan ketika mereka nanti diisi. Yang mengatakan, array yang berlubang bukanlah alasan untuk masalah kinerja dalam kasus ini; itu hanya berarti kita harus melakukan pemeriksaan Smi ekstra pada setiap iterasi (untuk menjaga dari lubang), yang bukan masalah besar.TL; DR Loop yang lebih lambat adalah karena mengakses Array 'out-of-bounds', yang memaksa engine untuk mengkompilasi ulang fungsi dengan sedikit atau bahkan tanpa optimasi ATAU untuk tidak mengkompilasi fungsi dengan salah satu dari optimasi ini untuk memulai dengan ( jika (JIT-) Compiler mendeteksi / mencurigai kondisi ini sebelum 'versi' kompilasi pertama), baca di bawah mengapa;
Seseorang hanya harus mengatakan ini (benar-benar kagum tidak ada yang melakukannya):
Dulu ada waktu ketika potongan OP akan menjadi contoh de-facto dalam buku pemrograman pemula yang dimaksudkan untuk menguraikan / menekankan bahwa 'array' dalam javascript diindeks mulai pada 0, bukan 1, dan karenanya digunakan sebagai contoh umum 'kesalahan pemula' (jangan Anda suka bagaimana saya menghindari frasa 'kesalahan pemrograman'
;)
): out-of-bounds Akses array .Contoh 1:
a
Dense Array
(bersebelahan (berarti tidak ada kesenjangan antara indeks) DAN sebenarnya elemen pada setiap indeks) dari 5 elemen menggunakan pengindeksan berbasis 0 (selalu dalam ES262).Dengan demikian kita tidak benar-benar berbicara tentang perbedaan kinerja antara
<
vs<=
(atau 'satu iterasi ekstra'), tetapi kita berbicara:'mengapa snipet yang benar (b) berjalan lebih cepat daripada snipet yang salah (a)'?
Jawabannya adalah 2 kali lipat (meskipun dari perspektif pelaksana bahasa ES262 keduanya adalah bentuk optimasi):
Butir 1 cukup (dan benar IMHO) dijelaskan oleh jawaban yang diterima , tetapi itu hanya menghabiskan 2 kata ('kode') pada Butir 2: kompilasi .
Lebih tepatnya: JIT-Kompilasi dan bahkan yang lebih penting JIT- RE- Kompilasi!
Spesifikasi bahasa pada dasarnya hanyalah deskripsi dari serangkaian algoritma ('langkah-langkah untuk melakukan untuk mencapai hasil akhir yang ditentukan'). Yang ternyata adalah cara yang sangat indah untuk menggambarkan suatu bahasa. Dan itu meninggalkan metode aktual yang digunakan mesin untuk mencapai hasil yang ditentukan terbuka untuk para pelaksana, memberikan banyak peluang untuk menghasilkan cara yang lebih efisien untuk menghasilkan hasil yang ditentukan. Mesin yang memenuhi spesifikasi harus memberikan hasil yang sesuai dengan spesifikasi untuk setiap input yang ditentukan.
Sekarang, dengan kode javascript / perpustakaan / penggunaan meningkat, dan mengingat berapa banyak sumber daya (waktu / memori / dll) yang digunakan kompiler 'nyata', jelas kami tidak dapat membuat pengguna yang mengunjungi halaman web menunggu selama itu (dan membutuhkannya untuk memiliki banyak sumber daya yang tersedia).
Bayangkan fungsi sederhana berikut:
Jelas sempurna, bukan? Tidak memerlukan klarifikasi tambahan APAPUN, Benar? Jenis kembali adalah
Number
, kan?Ya .. tidak, tidak & tidak ... Itu tergantung pada argumen apa yang Anda berikan ke parameter fungsi bernama
arr
...Lihat masalahnya? Kalau begitu anggap ini hanyalah permutasi besar yang nyaris mustahil ... Kita bahkan tidak tahu jenis TIPE fungsi KEMBALI sampai kita selesai ...
Sekarang bayangkan kode fungsi yang sama ini benar-benar digunakan pada tipe yang berbeda atau bahkan variasi input, keduanya secara harfiah (dalam kode sumber) dijelaskan dan secara dinamis dalam program 'array' dihasilkan.
Dengan demikian, jika Anda mengkompilasi fungsi
sum
JUST ONCE, maka satu-satunya cara yang selalu mengembalikan hasil yang ditentukan oleh spesifikasi untuk setiap dan semua jenis input, tentu saja, hanya dengan melakukan SEMUA langkah utama DAN sub-resep yang ditentukan spesifik yang dapat menjamin hasil yang sesuai dengan spesifikasi (seperti browser pra-y2k yang tidak disebutkan namanya). Tidak ada optimasi (karena tidak ada asumsi) dan bahasa scripting lambat ditafsirkan lambat.Kompilasi JIT (JIT seperti dalam Just In Time) adalah solusi populer saat ini.
Jadi, Anda mulai mengkompilasi fungsi menggunakan asumsi tentang apa yang dilakukannya, kembali dan menerima.
Anda datang dengan pemeriksaan sesederhana mungkin untuk mendeteksi jika fungsi tersebut mungkin mulai mengembalikan hasil yang tidak spesifik (seperti karena ia menerima input yang tidak terduga). Kemudian, buang hasil kompilasi sebelumnya dan kompilasi ulang ke sesuatu yang lebih rumit, putuskan apa yang harus dilakukan dengan hasil parsial yang sudah Anda miliki (apakah valid untuk dipercaya atau dihitung lagi untuk memastikan), ikat fungsi kembali ke dalam program dan coba lagi. Akhirnya jatuh kembali ke penafsiran naskah bertahap seperti dalam spec.
Semua ini membutuhkan waktu!
Semua browser bekerja pada mesin mereka, untuk masing-masing dan setiap sub-versi Anda akan melihat semuanya membaik dan mundur. String pada suatu saat dalam string benar-benar abadi (maka array.join lebih cepat dari penggabungan string), sekarang kita menggunakan tali (atau serupa) yang meringankan masalah. Keduanya mengembalikan hasil yang sesuai dengan spesifikasi dan itulah yang penting!
Singkatnya: hanya karena semantik bahasa javascript sering kali mendukung kita (seperti bug diam ini dalam contoh OP) tidak berarti bahwa kesalahan 'bodoh' meningkatkan peluang kompiler mengeluarkan kode mesin cepat. Diasumsikan kita menulis 'biasanya' instruksi yang benar: mantra saat ini yang kita 'pengguna' (dari bahasa pemrograman) harus miliki adalah: membantu kompiler, menggambarkan apa yang kita inginkan, mendukung idiom umum (mengambil petunjuk dari asm.js untuk pemahaman dasar browser apa yang bisa dioptimalkan dan mengapa).
Karena itu, berbicara tentang kinerja sama pentingnya, TETAPI JUGA sebuah ladang ranjau (dan karena ladang ranjau itu, saya benar-benar ingin mengakhiri dengan menunjuk ke (dan mengutip) beberapa materi yang relevan:
Sumber:
"JITProf: Penentuan Kode JavaScript JIT-tidak ramah"
publikasi Berkeley, 2014, oleh Liang Gong, Michael Pradel, Koushik Sen.
http://software-lab.org/publications/jitprof_tr_aug3_2014.pdf
ASM.JS (juga tidak suka keluar dari akses array terikat):
http://asmjs.org/spec/latest/
dan akhirnya https://blogs.windows.com/msedgedev/2015/05/07/membawa-asm-js-to-chakra-microsoft-edge/
jika ada subbagian kecil tentang peningkatan kinerja internal mesin ketika melepas batas- check (sementara hanya mengangkat batas-periksa di luar loop sudah memiliki peningkatan 40%).
EDIT:
perhatikan bahwa banyak sumber berbicara tentang berbagai tingkat JIT-Rekompilasi hingga interpretasi.
Contoh teoritis berdasarkan informasi di atas, mengenai cuplikan OP:
Karenanya waktu adalah:
Jalankan pertama (gagal pada akhirnya) + melakukan semua pekerjaan lagi menggunakan kode mesin lebih lambat untuk setiap iterasi + kompilasi dll. Jelas membutuhkan> 2 kali lebih lama dalam contoh teoritis ini !
EDIT 2: (penafian: dugaan berdasarkan fakta-fakta di bawah)
Semakin saya memikirkannya, semakin saya berpikir bahwa jawaban ini sebenarnya dapat menjelaskan alasan yang lebih dominan untuk 'hukuman' ini pada cuplikan yang salah (atau bonus kinerja pada cuplikan) , tergantung pada bagaimana Anda memikirkannya), tepatnya mengapa saya adament dalam menyebutnya (snippet a) kesalahan pemrograman:
Cukup menggoda untuk menganggap bahwa itu
this.primes
adalah numerik murni 'array padat' yang juganew Array(/*size value*/)
) dalam urutan berurutan (kandidat lama yang dikenal untuk menjadi array 'nyata').Kita juga tahu bahwa
primes
panjang array di- cache sebagaiprime_count
! (menunjukkan niat dan ukuran tetap).Kita juga tahu bahwa kebanyakan mesin pada awalnya mengeluarkan Array sebagai copy-on-modifikasi (bila perlu) yang membuat penanganannya jauh lebih cepat (jika Anda tidak mengubahnya).
Oleh karena itu masuk akal untuk mengasumsikan bahwa Array
primes
kemungkinan besar sudah merupakan array yang dioptimalkan secara internal yang tidak berubah setelah penciptaan (mudah diketahui untuk kompiler jika tidak ada kode yang memodifikasi array setelah penciptaan) dan oleh karena itu sudah (jika berlaku untuk mesin) disimpan dengan cara yang dioptimalkan, cukup banyak seolah-olah itu adalahTyped Array
.Seperti yang saya coba jelaskan dengan
sum
contoh fungsi saya , argumen yang dilewati sangat mempengaruhi apa yang sebenarnya perlu terjadi dan bagaimana kode tertentu dikompilasi ke kode mesin. Melewati aString
kesum
fungsi seharusnya tidak mengubah string tetapi mengubah bagaimana fungsi dikompilasi JIT! Melewati Array untuksum
mengkompilasi versi mesin-kode yang berbeda (mungkin bahkan tambahan untuk jenis ini, atau 'bentuk').Karena sepertinya sedikit bonkus untuk mengonversi
primes
Array Typed_Array-like on-the-fly ke something_else sementara kompiler tahu fungsi ini bahkan tidak akan memodifikasinya!Di bawah asumsi ini, ada 2 opsi:
Saya sekarang benar-benar bertanya-tanya yang mana dari 2 ini!
sumber
Untuk menambahkan sedikit keilmiahan padanya, inilah jsperf
https://jsperf.com/ints-values-in-out-of-array-bounds
Ini menguji kasus kontrol dari array yang diisi dengan int dan perulangan melakukan aritmatika modular sambil tetap dalam batas. Ini memiliki 5 kasus uji:
new Array()
Ini menunjukkan bahwa 4 kasus pertama sangat buruk untuk kinerja. Looping di luar batas sedikit lebih baik daripada 3 lainnya, tetapi semua 4 kira-kira 98% lebih lambat dari kasus terbaik.
The
new Array()
kasus hampir sama baiknya dengan array baku, hanya beberapa persen lebih lambat.sumber