TL; DR Daha yavaş döngü, motoru daha az optimizasyonla veya daha az optimizasyonla yeniden derlemeye zorlayan VEYA fonksiyonu bu optimizasyonlardan herhangi biriyle derlememeye zorlayan Array 'sınırların dışında' erişime bağlıdır. (JIT-) Derleyicisi ilk derleme 'sürümünden önce bu durumu tespit etti / şüphelendiğinde), nedenini aşağıda okuyun;
Birisi sadece
vardır bu (tamamen hayrete kimse zaten yaptım) demek:
OP'ın pasajı programlama kitabı anahat amaçlanan bir başlayanlar bir fiili örnek olacağını bir zaman olması için kullanılır / javascript 'diziler' endeksli bir başlangıç olduğunu vurgulamak 0'da değil, 1'de olduğu gibi yaygın bir 'yeni başlayanlar hatasına' bir örnek olarak kullanılabilir ('programlama hatası' ifadesinden nasıl kaçındığımı sevmezsiniz
;)
):
sınırların dışında Dizi erişimi .
Örnek 1:
Bir Dense Array
(her zaman ES262 olarak) 0 tabanlı dizin kullanılarak 5 elemanları (aslında bitişik (indeksleri arasında boşlukları anlamına gelir) ve her bir dizin bir elemanı olan).
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
Dolayısıyla, <
vs <=
(veya 'bir ekstra yineleme') arasındaki performans farkı hakkında gerçekten konuşmuyoruz , ancak şu şekilde konuşuyoruz:
'Doğru snippet (b) neden hatalı snippet (a)' dan daha hızlı çalışıyor? '
Cevap 2 kattır (ES262 dil uygulayıcının bakış açısından her ikisi de optimizasyon formlarıdır):
- Veri Gösterimi: Diziyi dahili olarak bellekte temsil etme / saklama (nesne, hashmap, 'gerçek' sayısal dizi, vb.)
- Fonksiyonel Makine kodu: bu 'Dizilere' erişen / işleyen kodun derlenmesi (okunması / değiştirilmesi)
Madde 1, kabul edilen cevapla yeterince (ve doğru IMHO) açıklanmıştır , ancak bu Madde 2: derlemeye sadece 2 kelime ('kod') harcar .
Daha doğrusu: JIT-Derleme ve daha da önemlisi JIT- RE -Derleme !
Dil belirtimi temel olarak sadece bir dizi algoritmanın tanımıdır ('tanımlı nihai sonuca ulaşmak için gerçekleştirilecek adımlar'). Bu, ortaya çıktığı gibi bir dili tanımlamanın çok güzel bir yoludur. Ve bir motorun uygulayıcılara açık olan belirli sonuçları elde etmek için kullandığı gerçek yöntemi terk ederek, tanımlı sonuçlar üretmek için daha verimli yollar bulma fırsatı sunar. Spesifikasyona uygun bir motor, tanımlanan herhangi bir giriş için spesifikasyonlu sonuçlar vermelidir.
Şimdi, javascript kodu / kitaplıkları / kullanımı arttıkça ve bir 'gerçek' derleyicinin ne kadar kaynak kullandığını (zaman / bellek / vb.) Hatırlayarak, bir web sayfasını ziyaret eden kullanıcıların bu kadar beklemesini sağlayamayacağımız açıktır (ve bunlara gereksinim duyuyoruz bu kadar çok kaynağa sahip olmak için).
Aşağıdaki basit işlevi düşünün:
function sum(arr){
var r=0, i=0;
for(;i<arr.length;) r+=arr[i++];
return r;
}
Tamamen açık, değil mi? Herhangi bir ekstra açıklama gerektirmez, değil mi? Dönüş türü Number
, değil mi?
Şey .. hayır, hayır ve hayır ... Adlandırılmış fonksiyon parametresine hangi argümanı ilettiğinize bağlı 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)
Sorunu görüyor musunuz? O zaman bunun büyük olası permütasyonları zar zor kazıdığını düşünün ... Yapana kadar RETURN işlevinin ne tür bir TİP olduğunu bilmiyoruz bile ...
Şimdi, aynı fonksiyon kodunun aslında tam anlamıyla (kaynak kodunda) açıklanan ve dinamik olarak program içinde üretilen 'diziler' gibi farklı tiplerde ve hatta giriş varyasyonlarında kullanıldığını hayal edin .
Bu nedenle, sum
SADECE ONCE işlevini derleyecekseniz , her türlü girdi türü için her zaman spec tanımlı sonucu döndüren tek yol, daha sonra, sadece TÜM spec tarafından belirtilen ana VE alt adımları gerçekleştirerek spesifikasyona uygun sonuçları garanti edebilir (adsız bir y2k tarayıcısı gibi). Optimizasyon yok (çünkü varsayımlar yok) ve yavaş yavaş yorumlanmış komut dosyası dili kalıyor.
JIT-Compilation (Tam Zamanında olduğu gibi JIT) mevcut popüler çözümdür.
Böylece işlevi, yaptığı, döndürdüğü ve kabul ettiği varsayımları kullanarak derlemeye başlarsınız.
işlevin spesifik olmayan uygun olmayan sonuçlar döndürmeye başlayıp başlamayacağını tespit etmek için mümkün olduğunca basit kontroller yaparsınız (beklenmedik bir girdi aldığı için olduğu gibi). Ardından, önceki derlenmiş sonucu atın ve daha ayrıntılı bir şeye yeniden derleyin, zaten sahip olduğunuz kısmi sonuçla ne yapacağınıza karar verin (güvenilir olmak veya emin olmak için tekrar hesaplamak geçerli mi), işlevi programa geri bağlayın ve Tekrar deneyin. Nihayetinde spesifikasyondaki gibi adım adım senaryo yorumlamasına geri dönüyoruz.
Bütün bunlar zaman alır!
Tüm tarayıcılar motorlarında çalışır, her alt sürüm için her şeyin geliştiğini ve gerilediğini göreceksiniz. Dizeler tarihin bir noktasında gerçekten değişmez dizelerdi (bu nedenle array.join, dize birleşiminden daha hızlıydı), şimdi sorunu hafifleten halatlar (veya benzeri) kullanıyoruz. Her ikisi de spesifikasyona uygun sonuçlar verir ve önemli olan budur!
Uzun lafın kısası: javascript'in dilinin anlambiliminin sık sık sırtımızı alması (OP'nin örneğindeki bu sessiz hatada olduğu gibi), 'aptal' hataların derleyicinin hızlı makine kodu tükürme şansını artırdığı anlamına gelmez. 'Genellikle' doğru talimatları yazdığımızı varsayar: 'kullanıcılarımızın' (programlama dilinin) sahip olması gereken şu mantra: derleyiciye yardım et, ne istediğimizi tarif et, ortak deyimleri destekle (temel anlayış için asm.js'den ipuçları al) hangi tarayıcıları optimize etmeye çalışabilir ve neden).
Bu nedenle, performanstan bahsetmek hem önemlidir, ancak AYRICA bir mayın tarlası (ve söz konusu mayın tarlası nedeniyle gerçekten ilgili bazı materyalleri işaret etmek (ve alıntılamak) ile bitirmek istiyorum:
Varolmayan nesne özelliklerine ve sınırların dışındaki dizi öğelerine erişim, undefined
bir istisna oluşturmak yerine değeri döndürür . Bu dinamik özellikler JavaScript'te programlamayı kolaylaştırır, ancak JavaScript'in verimli makine koduna derlenmesini de zorlaştırır.
...
Etkili JIT optimizasyonu için önemli bir öneri, programcıların JavaScript'in dinamik özelliklerini sistematik bir şekilde kullanmasıdır. Örneğin, JIT derleyicileri, nesne özelliklerinin genellikle belirli bir sırada belirli bir türdeki bir nesneye eklendiğinden veya sınır dışı dizi erişimlerinin nadiren gerçekleşmesinden yararlanır. JIT derleyicileri, çalışma zamanında verimli makine kodu oluşturmak için bu düzenlilik varsayımlarından yararlanır. Bir kod bloğu varsayımları karşılarsa, JavaScript motoru verimli, oluşturulan makine kodunu yürütür. Aksi takdirde, motor daha yavaş koda veya programı yorumlamaya geri dönmelidir.
Kaynak:
"JITProf: JIT dostu olmayan JavaScript Kodunu saptamak"
Berkeley yayını, 2014, Liang Gong, Michael Pradel, Koushik Sen.
http://software-lab.org/publications/jitprof_tr_aug3_2014.pdf
ASM.JS (ayrıca dizi erişiminden de hoşlanmıyor):
Önceden Derleme
Asm.js, JavaScript'in katı bir alt kümesi olduğundan, bu belirtim yalnızca doğrulama mantığını tanımlar; yürütme semantiği yalnızca JavaScript'tir. Bununla birlikte, doğrulanmış asm.js, vaktinden önce (AOT) derlemeye uygundur. Dahası, bir AOT derleyicisi tarafından üretilen kod, aşağıdakileri içeren oldukça verimli olabilir:
- tam sayıların ve kayan nokta sayılarının kutulanmamış gösterimleri;
- çalışma zamanı türü denetimlerinin olmaması;
- çöp toplama yokluğu; ve
- Etkin yığın yükleri ve depoları (platforma göre değişen uygulama stratejileriyle).
Doğrulamayan kod, yorumlama ve / veya tam zamanında (JIT) derleme gibi geleneksel yollarla yürütmeye geri dönmelidir.
http://asmjs.org/spec/latest/
ve son olarak https://blogs.windows.com/msedgedev/2015/05/07/bringing-asm-js-to-chakra-microsoft-edge/ sınırları
kaldırırken motorun dahili performans iyileştirmeleri hakkında küçük bir alt bölüm vardı. kontrol edin (sadece sınırların kaldırılması-döngü dışında kontrolün zaten% 40'lık bir iyileşmesi vardı).
EDIT:
birden fazla kaynağın yorumlama için farklı JIT-Recompilation düzeyleri hakkında konuştuğunu unutmayın.
OP'nin pasajına ilişkin yukarıdaki bilgilere dayanan teorik örnek :
- İsPrimeDivisible çağrısı
- Genel varsayımlar (sınır dışı erişim yok gibi) kullanılarak isPrimeDivisible'ı derleyin
- İşi yapmak
- BAM, aniden dizi sınırların dışına erişir (en sonunda).
- Crap, motor diyor ki, farklı (daha az) varsayımlar kullanarak bunuPrimeDivisible olarak yeniden derleyelim ve bu örnek motor, mevcut kısmi sonucu yeniden kullanıp kullanamayacağını anlamaya çalışmaz, bu yüzden
- Yavaş fonksiyon kullanarak tüm işleri yeniden hesaplayın (umarım biter, aksi takdirde tekrarlayın ve bu kez sadece kodu yorumlayın).
- Sonuç
Bu nedenle zaman:
İlk çalıştırma (sonunda başarısız oldu) + her bir yineleme için daha yavaş makine kodu kullanarak tekrar tekrar iş yapmak + yeniden derleme vb. Bu teorik örnekte açıkça> 2 kat daha uzun sürer !
DÜZENLEME 2: (feragatname: aşağıdaki gerçeklere dayalı varsayım)
Ne kadar çok düşünürsem, bu cevabın aslında bu snippet a (veya snippet b'deki performans bonusu) üzerindeki bu “ceza” nın daha baskın nedenini açıklayabileceğini düşünüyorum. , nasıl düşündüğünüze bağlı olarak), tam olarak neden bir programlama hatası (snippet a) deme konusunda kararlıyım:
this.primes
Bir 'yoğun dizi' saf sayısal olduğunu varsaymak oldukça cazip.
- Kaynak koddaki sabit kodlu hazır bilgi ( derleme zamanından önce derleyici tarafından her şey zaten bilindiği için 'gerçek' bir dizi haline geldiği bilinen mükemmel aday ) VEYA
- büyük olasılıkla
new Array(/*size value*/)
, artan sıralı sırada ('gerçek' bir dizi olmak için bilinen bir başka uzun zamandır bilinen aday) önceden boyutlandırılmış ( ) bir sayısal işlev kullanılarak oluşturulur .
Biz de biliyoruz primes
dizinin length olduğu önbelleğe olarak prime_count
! (niyetini ve sabit boyutunu gösterir).
Ayrıca çoğu motorun başlangıçta Dizileri kopyala-değiştir (gerektiğinde) olarak geçirdiğini biliyoruz, bu da onları daha hızlı teslim etmenizi sağlıyor (değiştirmezseniz).
Bu nedenle, Array'ın primes
büyük olasılıkla dahili olarak zaten oluşturulduktan sonra değiştirilmeden optimize edilmiş bir dizi olduğunu varsaymak mantıklıdır (oluşturma işleminden sonra diziyi değiştiren kod yoksa derleyici için bilmek kolaydır) ve bu nedenle zaten (varsa) motor) optimize edilmiş bir şekilde depolanır, sanki bir a Typed Array
.
sum
İşlev örneğimi açıklığa kavuşturmaya çalıştığım gibi, iletilen argümanlar gerçekte ne olması gerektiğini ve bu kodun makine koduna nasıl derlendiğini etkiler. Bir geçme String
için sum
fonksiyonun işlevi JIT-Derleyen nasıl dize ancak değişiklik değiştirmemelisiniz! Bir Array öğesinin sum
geçirilmesi, makine kodunun farklı bir sürümünü (belki de bu tür için ek olarak veya dediği gibi 'şekil') derlemelidir.
primes
Derleyici bu işlevin onu değiştirmeyeceğini bile bilirken, Typed_Array benzeri Diziyi anında bir şey_seçene dönüştürmek biraz güç gibi görünüyor !
2 seçenek bırakan bu varsayımlar altında:
- Sayı sınırlaması olmayan sayı olarak derleyin, sonunda sınır dışı sorunla karşılaşın, çalışmayı yeniden derleyin ve yineleyin (yukarıdaki düzenleme 1'de teorik örnekte belirtildiği gibi)
- Derleyici, önceden bağlanmış girişlerin dışında zaten algıladı (veya şüphelendi mi?) Ve işlev, geçen argüman, daha yavaş işlevsel makine koduyla sonuçlanan (daha fazla kontrol / dönüşüm / zorlama olacağı gibi) seyrek bir nesne gibi JIT-Derlendi vb.). Başka bir deyişle: işlev hiçbir zaman belirli optimizasyonlar için uygun değildi, sanki bir 'seyrek dizi' (- gibi) argümanı almış gibi derlendi.
Şimdi gerçekten bu 2 hangisinin olduğunu merak ediyorum!
<=
ve<
aynıdır.