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).
var arr_five_char=['a', 'b', 'c', 'd', 'e']; // arr_five_char.length === 5
// indexes are: 0 , 1 , 2 , 3 , 4 // there is NO index number 5
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):
- Representasi Data: bagaimana merepresentasikan / menyimpan Array secara internal dalam memori (objek, hashmap, array numerik 'nyata', dll.)
- Fungsional Mesin-kode: cara mengkompilasi kode yang mengakses / menangani (baca / modifikasi) 'Array' ini
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:
function sum(arr){
var r=0, i=0;
for(;i<arr.length;) r+=arr[i++];
return r;
}
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
...
sum('abcde'); // String('0abcde')
sum([1,2,3]); // Number(6)
sum([1,,3]); // Number(NaN)
sum(['1',,3]); // String('01undefined3')
sum([1,,'3']); // String('NaN3')
sum([1,2,{valueOf:function(){return this.val}, val:6}]); // Number(9)
var val=5; sum([1,2,{valueOf:function(){return val}}]); // Number(8)
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:
Akses ke properti objek yang tidak ada dan elemen array di luar batas mengembalikan undefined
nilai alih-alih menaikkan pengecualian. Fitur dinamis ini membuat pemrograman dalam JavaScript nyaman, tetapi mereka juga membuatnya sulit untuk mengkompilasi JavaScript ke dalam kode mesin yang efisien.
...
Premis penting untuk optimalisasi JIT yang efektif adalah bahwa pemrogram menggunakan fitur dinamis JavaScript secara sistematis. Sebagai contoh, kompiler JIT mengeksploitasi fakta bahwa properti objek sering ditambahkan ke objek dengan tipe tertentu dalam urutan tertentu atau jarang diakses oleh array array. Kompiler JIT mengeksploitasi asumsi keteraturan ini untuk menghasilkan kode mesin yang efisien saat runtime. Jika suatu blok kode memenuhi asumsi, mesin JavaScript mengeksekusi kode mesin yang efisien dan dihasilkan. Kalau tidak, mesin harus kembali ke kode lebih lambat atau menafsirkan program.
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):
Kompilasi Sebelumnya
Karena asm.js adalah subset ketat dari JavaScript, spesifikasi ini hanya mendefinisikan logika validasi — semantik pelaksanaannya hanyalah JavaScript. Namun, asm.js yang divalidasi dapat menerima kompilasi sebelumnya (AOT). Selain itu, kode yang dihasilkan oleh kompiler AOT bisa sangat efisien, menampilkan:
- representasi bilangan bulat dan angka floating-point tanpa kotak;
- tidak adanya pemeriksaan tipe runtime;
- tidak adanya pengumpulan sampah; dan
- tumpukan dan toko yang efisien (dengan strategi implementasi berbeda-beda berdasarkan platform).
Kode yang gagal divalidasi harus kembali ke eksekusi dengan cara tradisional, mis. Kompilasi interpretasi dan / atau just-in-time (JIT).
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:
- Panggil ke isPrimeDivisible
- Kompilasi isPrimeDivisible menggunakan asumsi umum (seperti tidak ada akses di luar batas)
- Bekerja
- BAM, tiba-tiba array mengakses di luar batas (tepat di akhir).
- Omong-omong, kata engine, mari kita kompilasi ulang yaituPrimeDivisible menggunakan berbagai asumsi (kurang), dan mesin contoh ini tidak mencoba mencari tahu apakah ia dapat menggunakan kembali hasil parsial saat ini, jadi
- Hitung ulang semua pekerjaan menggunakan fungsi lebih lambat (mudah-mudahan selesai, jika tidak ulangi dan kali ini hanya menafsirkan kode).
- Hasil kembali
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 juga
- Hard-code literal dalam source-code (kandidat yang dikenal unggul untuk menjadi array 'nyata' karena semuanya sudah diketahui oleh kompiler sebelum waktu kompilasi) ATAU
- kemungkinan besar dihasilkan menggunakan fungsi numerik mengisi pra-ukuran (
new Array(/*size value*/)
) dalam urutan berurutan (kandidat lama yang dikenal untuk menjadi array 'nyata').
Kita juga tahu bahwa primes
panjang array di- cache sebagai prime_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 adalah Typed 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 a String
ke sum
fungsi seharusnya tidak mengubah string tetapi mengubah bagaimana fungsi dikompilasi JIT! Melewati Array untuk sum
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:
- Kompilasi sebagai pengurai angka dengan asumsi tidak ada batas, mengalami masalah di luar batas, kompilasi ulang dan ulangi pekerjaan (seperti diuraikan dalam contoh teoretis dalam edit 1 di atas)
- Kompiler telah mendeteksi (atau diduga?) Keluar dari akses terikat di muka dan fungsinya dikompilasi JIT seolah argumen yang dilewati adalah objek yang jarang menghasilkan kode mesin fungsional yang lebih lambat (karena akan memiliki lebih banyak pemeriksaan / konversi / pemaksaan) dll.) Dengan kata lain: fungsi tidak pernah memenuhi syarat untuk optimisasi tertentu, itu dikompilasi seolah-olah ia menerima argumen '(seperti) array jarang.
Saya sekarang benar-benar bertanya-tanya yang mana dari 2 ini!
<=
dan<
identik, baik dalam teori maupun dalam implementasi aktual di semua prosesor modern (dan juru bahasa).