Deoptimize edilmeden yüksek performanslı Javascript kodu yazma


10

Javascript'te büyük sayısal diziler üzerinde çalışan (tamsayılar veya kayan nokta sayıları üzerinde çalışan doğrusal bir cebir paketi düşünün) performansa duyarlı kod yazarken, JIT'in her zaman olabildiğince yardımcı olmasını ister. Kabaca bu şu anlama gelir:

  1. Tamsayı mı yoksa kayan nokta hesaplaması mı yaptığımıza bağlı olarak dizilerimizin her zaman SMI'ların (küçük tamsayılar) veya paketlenmiş Çiftler olmasını istiyoruz.
  2. Her zaman işlevlere aynı türden bir şey iletmek istiyoruz, böylece "megamorfik" olarak etiketlenip deoptimize edilmiyorlar. Örneğin, her zaman arayarak olmak istiyorum vec.add(x, y)hem xve ypaketlenmiş SMI dizileri olmak ya da her ikisi çift dizileri dolu.
  3. Fonksiyonların olabildiğince sıralı olmasını istiyoruz.

Biri bu vakaların dışında kaldığında, ani ve sert bir performans düşmesi meydana gelir. Bu çeşitli zararsız nedenlerle olabilir:

  1. Paketlenmiş bir SMI dizisini, eşdeğeri gibi, görünüşte zararsız bir işlemle paketlenmiş bir Çift diziye dönüştürebilirsiniz myArray.map(x => -x). Paketlenmiş Çift diziler hala çok hızlı olduğu için bu aslında "en iyi" kötü durumdur.
  2. Paketlenmiş bir diziyi genel kutulu bir diziye dönüştürebilirsiniz, örneğin diziyi (beklenmedik şekilde) döndürülen bir işlev üzerinde eşleyerek nullveya undefined. Bu kötü durumdan kaçınmak oldukça kolaydır.
  3. vec.add()Çok fazla şey geçirerek ve onu megamorfik hale getirerek, bütün bir işlevi deoptimize edebilirsiniz. Bu, vec.add()hem türler konusunda dikkatli olmadığınız durumlarda (bu nedenle çok fazla türün geldiğini görür) hem de maksimum performans eklemek istediğiniz durumlarda "genel programlama" yapmak istediğinizde olabilir. (örneğin, yalnızca kutulu iki katına çıkmalıdır).

Benim sorum daha iyi bir soru, nasıl yüksek performanslı Javascript kodu yukarıdaki hususlar ışığında nasıl yazarken, yine de kodu güzel ve okunabilir tutar. Ne tür bir cevap hedeflediğimi bilmeniz için bazı özel alt sorular:

  • Paketlenmiş SMI dizileri dünyasında kalırken nasıl programlanacağına dair bir dizi yönerge var mı (örneğin)?
  • Javascript'te vec.add(), çağrı sitelerine benzer şeyleri satır içine almak için makro sistemi gibi bir şey kullanmadan genel yüksek performanslı programlama yapmak mümkün müdür?
  • Megamorfik çağrı siteleri ve deoptimizasyonlar gibi şeyler ışığında yüksek performanslı kodu nasıl libarilere dönüştürür? Örneğin A, yüksek hızda mutlu bir şekilde Doğrusal Cebir paketini kullanıyorsam ve daha sonra Bbağımlı olan Aancak Bdiğer türlerle çağıran ve deoptimize eden bir paketi içe aktarırsam, aniden (kodumu değiştirmeden) kodum daha yavaş çalışır.
  • Herhangi bir iyi var mıdır kullanımı kolay JavaScript motoru türleri ile dahili olarak ne yaptığını kontrol etmek için ölçüm araçları?

1
Bu çok ilginç bir konu ve araştırmanızı doğru bir şekilde yaptığınızı gösteren çok iyi yazılmış bir yazı. Bununla birlikte, soruların SO formatı için çok geniş olduğundan ve kaçınılmaz olarak gerçeklerden daha fazla fikir çekeceğinden korkuyorum. Kod optimizasyonu çok karmaşık bir konudur ve bir motorun iki versiyonu aynı şekilde davranmayabilir. Bence bazen takılan V8 JIT'den sorumlu kişilerden biri var, bu yüzden belki de motorları için uygun bir cevap verebilirler, ama onlar için bile, tek bir Q / A için bir konunun çok geniş olacağını düşünüyorum. .
Kaiido

"Sorum daha yumuşak bir soru, nasıl yüksek performanslı Javascript kodu yazma hakkında ..." Bir kenara, javascript arka plan süreçleri (web çalışanları) yumurtlama sağlar ve aynı zamanda içine dokunan kütüphaneler olduğunu unutmayın GPU (tensorflow.js ve gpu.js) sunan, sadece javascript tabanlı bir uygulamanın hesaplanan iş hacmini artırmak için derlemeye dayanmaktan başka araçlar sunmaktadır ...
Jon Trent

@JonTrent Aslında yazımda biraz yalan söyledim, klasik lineer cebir uygulamaları için çok fazla umursamıyorum, ancak tamsayılar üzerinde bilgisayar cebiri için daha fazla umurumda değil. Bu, birçok mevcut sayısal paketin hemen dışlandığı anlamına gelir, çünkü (örneğin) bir matrisi satır küçültürken 2'ye bölebilirler, bu da çalıştığım dünyada (1/2) beri "izin verilmez" bir tamsayı değil. Web çalışanlarını (özellikle iptal edilebilir olmak istediğim birkaç uzun süredir devam eden hesaplamalar için) düşündüm, ancak burada ele aldığım sorun, etkileşime yanıt verecek kadar gecikmeyi azaltıyor.
Joppy

JavaScript'te tamsayı aritmetiği için, muhtemelen asm.js tarzı koda bakıyorsunuz, kabaca " |0her işlemin arkasını koyuyorsunuz ". Güzel değil, ancak doğru tamsayıları olmayan bir dilde yapabileceğiniz en iyi şey. BigInts'i de kullanabilirsiniz, ancak bugün itibariyle ortak motorların hiçbirinde çok hızlı değiller (çoğunlukla talep eksikliği nedeniyle).
jmrk

Yanıtlar:


8

V8 geliştiricisi burada. Bu soruya olan ilgi miktarı ve diğer cevapların eksikliği göz önüne alındığında, bunu denebilirim; Korkarım ki umduğun cevap bu olmayacak.

Paketlenmiş SMI dizileri dünyasında kalırken nasıl programlanacağına dair bir dizi yönerge var mı (örneğin)?

Kısa cevap: işte burada: const guidelines = ["keep your integers small enough"].

Daha uzun cevap: çeşitli nedenlerle kapsamlı bir kılavuz seti vermek zordur. Genel olarak, JavaScript geliştiricilerinin kendileri ve kullanım durumları için anlamlı bir kod yazmaları ve JavaScript motoru geliştiricilerinin bu kodun motorlarında nasıl hızlı çalıştırılacağını anlamaları gerektiğidir. Kapak tarafında, motor uygulama tercihleri ​​ve optimizasyon çabalarına bakılmaksızın, bazı kodlama modellerinin her zaman diğerlerinden daha yüksek performans maliyetlerine sahip olacağı anlamında, bu ideale ilişkin bazı sınırlamalar vardır.

Performans tavsiyesi hakkında konuştuğumuzda, bunu aklımızda tutmaya çalışıyoruz ve hangi önerilerin birçok motorda ve uzun yıllar boyunca geçerli kalma olasılığının yüksek olduğunu ve makul olarak deyimsel / müdahaleci olmadığını dikkatli bir şekilde tahmin ediyoruz.

Eldeki örneğe dönersek: Smis'i dahili olarak kullanmanın, kullanıcı kodunun bilmesi gerekmeyen bir uygulama detayı olduğu varsayılır. Bazı vakaları daha verimli hale getirecek ve diğer durumlarda zarar vermemelidir. Tüm motorlar Smis kullanmaz (örneğin, AFAIK Firefox / Spidermonkey tarihsel olarak kullanmadı; Bazı durumlarda bu günlerde Smis kullandıklarını duydum; ancak hiçbir ayrıntı bilmiyorum ve herhangi bir otorite ile konuşamıyorum madde). V8'de Smis'in boyutu dahili bir ayrıntı ve aslında zamanla ve üzeri versiyonlarda değişiyor. Eskiden çoğunluk kullanım durumu olan 32 bit platformlarda, Smis her zaman 31 bit işaretli tam sayı olmuştur; 64 bit platformlarda eskiden en yaygın durum gibi görünen 32 bit işaretli tam sayılardı, Chrome 80'de "işaretçi sıkıştırması" gönderene kadar Smi boyutunu 32 bit platformlardan bilinen 31 bite indirmeyi gerektiren 64 bit mimariler için. Smis'in tipik olarak 32 bit olduğu varsayımına dayalı bir uygulama yapmış olsaydınız,bu .

Neyse ki, belirttiğiniz gibi, çift diziler hala çok hızlı. Sayısal-ağır kod için, çift dizileri varsaymak / hedeflemek muhtemelen mantıklıdır. JavaScript'te çiftlerin yaygınlığı göz önüne alındığında, tüm motorların çiftler ve çift diziler için iyi bir desteğe sahip olduğunu varsaymak mantıklıdır.

Vec.add () gibi şeyleri çağrı sitelerine satır içine almak için makro sistem gibi bir şey kullanmadan Javascript'te yüksek performanslı genel programlama yapmak mümkün müdür?

"jenerik" genellikle "yüksek performans" ile çelişmektedir. Bu JavaScript veya belirli motor uygulamaları ile ilgisi yoktur.

"Genel" kod, kararların çalışma zamanında alınması gerektiği anlamına gelir. Bir işlevi her yürüttüğünüzde, " xtamsayı mıdır? " Sorusunu belirlemek için kodun çalıştırılması gerekir. Öyleyse, bu kod yolunu alın. xBir dize mi? Sonra buraya atlayın. Bu bir nesne mi .valueOf? belki .toString()? Belki prototip zincirinde mi? Bunu çağırın ve sonucu ile baştan başlayın ". "Yüksek performanslı" optimize edilmiş kod esasen tüm bu dinamik kontrolleri düşürme fikri üzerine inşa edilmiştir; bu sadece motorun / derleyicinin tiplerini önceden çıkarmanın bir yolu olduğunda mümkündür: xher zaman bir tamsayı olacağını kanıtlayabilirse (veya yeterince yüksek olasılıkla varsayalım), bu durumda sadece kod üretmesi gerekir ( kanıtlanmamış varsayımların söz konusu olup olmadığına dair bir tür kontrol tarafından korunmaktadır).

Inlining tüm bunlara diktir. "Genel" bir işlev hala satır içine alınabilir. Bazı durumlarda, derleyici, burada polimorfizmi azaltmak için tip bilgisini satır içi fonksiyona yayabilir.

(Karşılaştırma için: C ++, statik olarak derlenmiş bir dil olarak, ilgili bir sorunu çözmek için şablonlara sahiptir. Kısacası, programcının derleyiciye belirli türlerde parametreleştirilmiş işlevlerin (veya tüm sınıfların) özel kopyalarını oluşturmasını açıkça bildirmesine izin verir. Örneğin bazı durumlarda için değil, sakıncaları kendi set vermeden güzel bir çözüm, uzun derleme kez ve büyük ikili. JavaScript, tabii ki, şablonlar diye bir şey vardır. Şunları kullanabilirsiniz evalsize daha sonra biraz benzer bir sistem inşa etmek, ancak benzer dezavantajlarla karşılaşırsanız: çalışma zamanında C ++ derleyicisinin çalışmasının eşdeğerini yapmanız ve oluşturduğunuz kod miktarı konusunda endişelenmeniz gerekir.)

Megamorfik çağrı siteleri ve deoptimizasyonlar gibi şeyler ışığında yüksek performanslı kodu nasıl libarilere dönüştürür? Örneğin, yüksek hızda mutlu bir şekilde Doğrusal Cebir A paketini kullanıyorsam ve sonra A'ya bağlı bir B paketini içe aktarırsam, ancak B diğer türlerle çağırır ve deoptimize eder, aniden (kodum değişmeden) kodum daha yavaş çalışır .

Evet, bu JavaScript ile ilgili genel bir sorundur. V8 Array.sort, JavaScript'te belirli yerleşikleri (şeyler gibi ) dahili olarak uygulardı ve bu sorun ("geri bildirim kirliliği" olarak adlandırdığımız), bu teknikten tamamen uzaklaşmamamızın temel nedenlerinden biriydi.

Bununla birlikte, sayısal kod için, o kadar çok tür (sadece Smis ve çiftler) yoktur ve belirttiğiniz gibi, pratikte benzer performansa sahip olmaları gerekir, bu nedenle tip geri besleme kirliliği gerçekten teorik bir endişe olur ve bazı durumlarda önemli bir etkiye sahipse, lineer cebir senaryolarında ölçülebilir bir fark görmemeniz de muhtemeldir.

Ayrıca, motorun içinde "bir tip == hızlı" ve "birden fazla tip == yavaş" durumundan çok daha fazla durum vardır. Belirli bir operasyon hem Smis'i hem de iki katını gördüyse, bu tamamen iyi. İki tür diziden eleman yüklemek de iyidir. Bir yükün, onları tek tek izlemekten vazgeçtiği kadar çok farklı tür gördüğü ve bunun yerine çok sayıda türe daha iyi ölçeklenen daha genel bir mekanizma kullandığı durum için "megamorfik" terimini kullanıyoruz - bu tür yükleri içeren bir işlev hala optimize edilebilir. Bir "deoptimizasyon", bir işlev için optimize edilmiş kodu atmak zorunda kalmanın çok özel bir eylemidir, çünkü daha önce görülmemiş ve optimize edilmiş kodun işlemek için donanımlı olmadığı yeni bir tür görülür. Ama bu bile iyi: daha fazla tür geribildirimi toplamak için optimize edilmemiş koda geri dönün ve daha sonra tekrar optimize edin. Bu birkaç kez olursa, endişelenecek bir şey yoktur; sadece patolojik olarak kötü durumlarda bir sorun haline gelir.

Yani tüm bunların özeti: endişelenmeyin . Sadece makul bir kod yazın, motorun bununla ilgilenmesine izin verin. Ve "makul" ile kastediyorum: kullanım durumunuz için anlamlı olan, okunabilir, bakımı kolay, verimli algoritmalar kullanır, dizilerin uzunluğunun ötesinde okuma gibi hatalar içermez. İdeal olarak, hepsi bu kadar, ve başka bir şey yapmanıza gerek yok. Eğer yapacak daha iyi hissetmeni sağlayacaksa şey , ve / veya aslında performans sorunları gözlemleyerek eğer, ben iki fikri sunabilir:

TypeScript kullanmak yardımcı olabilir . Büyük yağ uyarısı: TypeScript türleri, yürütme performansını değil geliştirici verimliliğini hedefler (ve ortaya çıktıkça, bu iki perspektifin bir tür sisteminden çok farklı gereksinimleri vardır). Bununla birlikte, bazı çakışmalar vardır: örneğin, sürekli olarak açıklama eklerseniz number, derleyici, nullyalnızca sayıları içermesi / çalıştırması gereken bir dizi veya işleve yanlışlıkla girerseniz sizi uyarır . Tabii ki, disiplin hala gereklidir: tek bir number_func(random_object as number)kaçış kapağı sessizce her şeyi zayıflatabilir, çünkü tip ek açıklamalarının doğruluğu hiçbir yerde uygulanmaz.

TypedArrays kullanmak da yardımcı olabilir. Normal JavaScript dizilerine kıyasla dizi başına biraz daha fazla ek yüke (bellek tüketimi ve ayırma hızı) sahiptir (bu nedenle, çok sayıda küçük diziye ihtiyacınız varsa, normal diziler muhtemelen daha verimlidir) ve daha az esnektirler çünkü büyümezler veya ayırmadan sonra küçülürler, ancak tüm elemanların tam olarak bir tipte olduğunu garanti ederler.

Javascript motorunun dahili olarak türlerle ne yaptığını kontrol etmek için kullanımı kolay ölçüm araçları var mı?

Hayır, bu kasıtlı. Yukarıda açıklandığı gibi, kodunuzu V8'in bugün özellikle iyi optimize edebileceği her şekle göre uyarlamanızı istemiyoruz ve bunu da gerçekten yapmak istediğinize inanmıyoruz. Bu şeyler her iki yönde de değişebilir: Kullanmak istediğiniz bir desen varsa, bunu gelecekteki bir sürümde optimize edebiliriz (daha önce kutulanmamış 32 bit tam sayıları dizi öğeleri olarak saklama fikriyle oynamıştık.) ama bununla ilgili çalışmalar henüz başlamadı, bu yüzden vaat yok); ve bazen geçmişte optimize etmek için kullandığımız bir model varsa, diğer daha önemli / etkili optimizasyonların önüne geçerse bunu bırakmaya karar verebiliriz. Ayrıca, satır içi sezgisel tarama gibi şeyleri doğru yapmak herkesin bildiği gibi zor, bu yüzden doğru satır içi kararı doğru zamanda almak devam eden bir araştırma alanı ve motor / derleyici davranışında bunlara karşılık gelen değişiklikler; bu da bunu herkes için talihsiz olacağı başka bir durum haline getiriyor (sizve biz) bazı mevcut tarayıcı sürümleri setini yaklaşık olarak düşündüğünüz (veya bildiğiniz?) en iyi karar verene kadar kodunuzu değiştirmek için çok fazla zaman harcadıysanız, yalnızca o zamanki mevcut tarayıcıları gerçekleştirmek için yarım yıl sonra geri gelmek buluşsal yöntemlerini değiştirdi.

Elbette, uygulamanızın performansını her zaman bir bütün olarak ölçebilirsiniz - sonuçta önemli olan budur, özellikle motorun dahili olarak yaptığı seçimler değil. Mikrobenchmarklara dikkat edin, çünkü yanıltıcıdırlar: sadece iki satır kod çıkarırsanız ve bunları karşılaştırırsanız, senaryonun motorun çok farklı kararlar vereceğinden yeterince farklı olması (örneğin, farklı tür geri bildirimleri).


2
Bu mükemmel yanıt için teşekkürler, bu işlerin nasıl hakkında benim şüpheler birçok onaylar ve onlar konum önemlisi nasıl amaçlanan işe. Bu arada, bahsettiğiniz "tip geri bildirim" sorunu hakkında herhangi bir blog yazısı vb. Var Array.sort()mı? Bununla ilgili biraz daha fazla okumak isterim.
Joppy

Bu konu hakkında blog yazdığımızı sanmıyorum. Esasen sorunuzda kendiniz tanımladığınız şey budur: yerleşik kodlar JavaScript'te uygulandığında, farklı kod parçaları farklı türlerle çağırırsa, performans düşebilir - bazen sadece biraz, bazen daha fazla. Bu teknikteki tek ve tartışmasız en büyük sorun bile değildi; Çoğunlukla genel konuya aşina olduğumu söylemek istedim.
jmrk
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.