Değişkenleri tekrar tekrar oluştursa da, ikinci işlev neden% 10 daha hızlı?


14
var toSizeString = (function() {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

  return function(size) {
    var gbSize = size / GB,
        gbMod  = size % GB,
        mbSize = gbMod / MB,
        mbMod  = gbMod % MB,
        kbSize = mbMod / KB;

    if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
    } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
    } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
    } else {
      return size + 'B';
    }
  };
})();

Ve daha hızlı fonksiyon: (her zaman aynı değişkenleri kb / mb / gb tekrar tekrar hesaplaması gerektiğini unutmayın). Nerede performans kazanıyor?

function toSizeString (size) {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

 var gbSize = size / GB,
     gbMod  = size % GB,
     mbSize = gbMod / MB,
     mbMod  = gbMod % MB,
     kbSize = mbMod / KB;

 if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
 } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
 } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
 } else {
      return size + 'B';
 }
};

3
Statik olarak yazılan herhangi bir dilde "değişkenler" sabit olarak derlenir. Belki modern JS motorları aynı optimizasyonu yapabilir. Değişkenler bir kapanışın parçasıysa, bu işe yaramaz gibi görünüyor.
usr

6
bu, kullandığınız JavaScript motorunun bir uygulama ayrıntısıdır. Teorik zaman ve mekan aynıdır, sadece bunları değiştirecek belirli bir JavaScript motorunun uygulanmasıdır. Sorunuzu doğru bir şekilde cevaplamak için, bunları ölçtüğünüz belirli JavaScript motorunu listelemeniz gerekir. Belki de birisi, nasıl diğerinin nasıl daha iyi hale getirildiğini söylemek için uygulanmasının ayrıntılarını bilir. Ayrıca ölçüm kodunuzu da göndermelisiniz.
Jimmy Hoffa

sabit değerlere referansla "compute" kelimesini kullanırsınız; orada referansta bulunduğunuz şeyde hesaplanacak hiçbir şey yoktur . Sabit değerlerin aritmetiği, derleyicilerin yaptığı en basit ve açık optimizasyonlardan biridir, bu nedenle yalnızca sabit değerlere sahip bir ifade gördüğünüzde, tüm ifadenin tek bir sabit değere optimize edildiğini varsayabilirsiniz.
Jimmy Hoffa

@JimmyHoffa doğru, ama öte yandan her fonksiyon çağrısında 3 sabit değişken yaratması gerekiyor ...
Tomy

@Tomy sabitleri değişken değildir. Değişmezler, bu yüzden derlemeden sonra yeniden yaratılmaları gerekmez. Bir sabit genellikle belleğe yerleştirilir ve bu sabit için gelecekteki her erişim tam olarak aynı yere yönlendirilir, onu yeniden oluşturmaya gerek yoktur, çünkü değeri asla değişmez , bu nedenle bir değişken değildir. Derleyiciler genellikle sabitler oluşturan kod yayınlamaz , derleyici oluşturma işlemini gerçekleştirir ve tüm kod referanslarını yaptıklarına yönlendirir.
Jimmy Hoffa

Yanıtlar:


23

Modern JavaScript motorlarının hepsi tam zamanında derleme yapar. "Tekrar tekrar yaratması gerekenler" hakkında herhangi bir varsayımda bulunamazsınız. Her iki durumda da, bu tür bir hesaplamanın optimize edilmesi nispeten kolaydır.

Öte yandan, sabit değişkenleri kapatmak JIT derlemesini hedefleyeceğiniz tipik bir durum değildir. Bu değişkenleri farklı çağrılarda değiştirmek istediğinizde genellikle bir kapak oluşturur. Ayrıca, bu değişkenlere erişmek için üye değişkene erişim ile OOP'de yerel int arasındaki fark gibi ek bir işaretçi referansı da oluşturuyorsunuz.

Bu tür bir durum insanların "erken optimizasyon" çizgisini atmasının nedenidir. Kolay optimizasyonlar derleyici tarafından zaten yapılmıştır.


Bahsettiğiniz gibi kayba neden olan değişken çözünürlük için kapsam geçişinin olduğundan şüpheleniyorum. Makul görünüyor, ama bir JavaScript JIT motorunda deliliğin ne olduğunu gerçekten bilen ...
Jimmy Hoffa

1
Bu cevabın olası genişlemesi: JIT'in çevrimdışı bir derleyici için kolay olan bir optimizasyonu göz ardı etmesinin nedeni , derleyicinin performansının olağandışı durumlardan daha önemli olmasıdır.
Leushenko

12

Değişkenler ucuzdur. Yürütme bağlamları ve kapsam zincirleri pahalıdır.

Temel olarak "çünkü kapanışlar" a kaybolan çeşitli cevaplar vardır ve bunlar esasen doğrudur, ancak problem özellikle kapanma ile ilgili değildir , değişkenleri farklı bir kapsamda referans alan bir fonksiyona sahip olmanızdır. Bunlar window, IIFE içindeki yerel değişkenlerin aksine , nesnede global değişkenler olsaydı aynı sorunu yaşarsınız. Deneyin ve görün.

İlk fonksiyonunuzda, motor bu ifadeyi gördüğünde:

var gbSize = size / GB;

Aşağıdaki adımları atmalıdır:

  1. sizeGeçerli kapsamda bir değişken arayın . (Buldum.)
  2. GBGeçerli kapsamda bir değişken arayın . (Bulunamadı.)
  3. GBÜst kapsamda bir değişken arayın . (Buldum.)
  4. Hesaplamayı yapın ve atayın gbSize.

Adım 3, sadece bir değişken tahsis etmekten çok daha pahalıdır. Üstelik bunu beş kez her ikisi için iki kez olmak üzere, GBve MB. Bunları işlevin başlangıcında (ör. var gb = GB) Taklit ettiyseniz ve bunun yerine takma ada başvurduysanız, bazı JS motorlarının zaten bu optimizasyonu gerçekleştirmesi mümkün olsa da, aslında küçük bir hızlanma üreteceğinden şüpheleniyorum . Ve elbette, uygulamayı hızlandırmanın en etkili yolu, kapsam zincirini hiç geçmemek.

JavaScript'in, derleyicinin bu değişken adreslerini derleme zamanında çözdüğü, derlenmiş, statik olarak yazılmış bir dil gibi olmadığını unutmayın. JS motoru bunları ada göre çözümlemek zorundadır ve bu aramalar her seferinde çalışma zamanında gerçekleşir. Yani mümkün olduğunca onlardan kaçınmak istersiniz.

Değişken atama JavaScript'te son derece ucuzdur. Bu ifadeyi destekleyecek hiçbir şeyim olmamasına rağmen, aslında en ucuz işlem olabilir. Bununla birlikte, değişken oluşturmaktan kaçınmaya çalışmak neredeyse hiç iyi bir fikir olmadığını söylemek güvenlidir ; bu alanda yapmaya çalıştığınız neredeyse tüm optimizasyonlar aslında işleri daha da kötüleştirecek, performans açısından.


"Optimizasyon" performansı olumsuz etkilemez bile, hemen hemen kesinlikle edilir olumsuz kod okunabilirliği etkileyecek. Hangi, bazı çılgın hesaplama şeyleri yapmazsanız, genellikle yapmak için kötü bir ödün (maalesef kalıcı bir bağlantı yok maalesef; "2009-02-17 11:41" arayın). Özet olarak: "Hız kesinlikle gerekli değilse, hız üzerinde netlik seçin."
CVn

Dinamik diller için çok temel bir tercüman yazarken bile, çalışma zamanı sırasında değişken erişim O (1) işlemi olma eğilimindedir ve ilk derleme sırasında O (n) kapsam geçişine bile gerek yoktur. Her kapsamda, yeni bildirilen her değişkene bir sayı atanır, bu nedenle olarak var a, b, cerişebiliriz . Tüm kapsamlar numaralandırılır ve bu kapsam beş kapsam derinliğinde iç içe yerleştirilmişse, ayrıştırma sırasında bilinen tam kapsamda ele alınır . Yerel kodda, kapsamlar yığın parçalarına karşılık gelir. Kapanışlar daha karmaşıktır, çünkü yedeklemeli ve değiştirilmelidirbscope[1]benv[5][1]env
amon

@amon: Bu nasıl şimdi etsen ideal olabilir gibi o işe, ama aslında nasıl çalıştığını değil. Bu konuda kitaplar yazdığımdan çok daha bilgili ve tecrübeli insanlar; özellikle sizi Nicholas C. Zakas'ın Yüksek Performanslı JavaScript'e yönlendirirdim. İşte bir pasaj ve ayrıca onu desteklemek için karşılaştırmalı değerlendirmeler yaptı. Tabii ki kesinlikle tek kişi değil, sadece en tanınmışı. JavaScript'in sözcüksel kapsamı vardır, bu nedenle kapaklar aslında o kadar da özel değildir - aslında her şey bir kapanıştır.
Aaronaught

@Aaronaught İlginç. Bu kitap 5 yaşında olduğundan, mevcut bir JS motorunun değişken aramaları nasıl ele aldığını ve V8 motorunun x64 arka ucuna nasıl baktığını merak ettim. Statik analiz sırasında, çoğu değişken statik olarak çözülür ve kapsamlarında bir bellek ofseti atanır. İşlev kapsamları bağlantılı listeler olarak temsil edilir ve montaj, doğru kapsama ulaşmak için açılmamış bir döngü olarak yayılır. Burada, *(scope->outer + variable_offset)bir erişim için C koduna eşdeğer oluruz ; her ekstra işlev kapsam düzeyi bir ek işaretçi dereference maliyeti. İkimiz de
haklıyız

2

Bir örnek bir kapatma içerir, diğeri içermez. Kapanışları uygulamak biraz zordur, çünkü değişkenler üzerinde kapalı normal değişkenler gibi çalışmaz. Bu, C gibi düşük seviyeli bir dilde daha belirgindir, ancak bunu göstermek için JavaScript kullanacağım.

Bir kapatma sadece bir fonksiyondan ibaret değildir, aynı zamanda kapattığı tüm değişkenlerden de oluşur. Bu işlevi çağırmak istediğimizde, tüm kapalı değişkenleri de sağlamalıyız. Bir kapatmayı, bu kapalı değişkenleri temsil eden ilk argüman olarak bir nesne alan bir işlevle modelleyebiliriz:

function add(vars, y) {
  vars.x += y;
}

function getSum(vars) {
  return vars.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(adder, 2);
console.log(adder.getSum(adder));  //=> 42

closure.apply(closure, ...realArgs)Bunun gerektirdiği garip çağrı kuralına dikkat edin

JavaScript'in yerleşik nesne desteği, açık varsargümanı atlamayı mümkün kılar ve thisbunun yerine kullanmamıza izin verir :

function add(y) {
  this.x += y;
}

function getSum() {
  return this.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

Bu örnekler aslında kapanışları kullanan bu koda eşdeğerdir:

function makeAdder(x) {
  return {
    add: function (y) { x += y },
    getSum: function () { return x },
  };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

Bu son örnekte, nesne yalnızca döndürülen iki işlevi gruplamak için kullanılır; thisbağlayıcı ilgisi yoktur. Kapakların mümkün kılınmasıyla ilgili tüm detaylar - gizli verilerin gerçek fonksiyona aktarılması, kapanış değişkenlerine tüm erişimlerin bu gizli verilerdeki aramalara değiştirilmesi - dil tarafından halledilir.

Ancak, çağrıları kapatmak, bu ekstra veriyi geçme yükünü içerir ve bir kapatma çalışması, bu önbellek konumu ve normal değişkenlerle karşılaştırıldığında genellikle bir işaretçi dereference tarafından daha da kötüleştirilen ekstra verilerdeki aramaların yükünü içerir; kapaklara dayanmayan bir çözüm daha iyi performans gösterir. Özellikle kapanışınızın size kaydettiği her şey, ayrıştırma sırasında sabit katlanmış olabilecek birkaç son derece ucuz aritmetik işlem olduğundan.

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.