dlaczego ta ostatnia funkcja jest 10% szybsza, chociaż musi ciągle tworzyć zmienne?

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';
    }
  };
})();

I szybsza funkcja: (należy pamiętać, że zawsze musi wyliczać ciągle te same zmienne kb / mb / gb). Gdzie zyskuje wydajność?

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';
 }
};
Do mojego
źródło
3
W każdym statycznie typowanym języku „zmienne” byłyby kompilowane jako stałe. Być może nowoczesne silniki JS są w stanie przeprowadzić tę samą optymalizację. To wydaje się nie działać, jeśli zmienne są częścią zamknięcia.
usr
6
jest to szczegół implementacji używanego silnika JavaScript. Teoretyczny czas i przestrzeń są takie same, tylko implementacja danego silnika JavaScript może to zmienić. Aby więc poprawnie odpowiedzieć na twoje pytanie, musisz podać konkretny silnik JavaScript, którym je mierzyłeś. Być może ktoś zna szczegóły jego implementacji, aby powiedzieć, w jaki sposób / dlaczego uczyniło to jedno bardziej optymalnym od drugiego. Powinieneś także opublikować swój kod pomiarowy.
Jimmy Hoffa
używasz słowa „oblicz” w odniesieniu do stałych wartości; naprawdę nie ma tam nic do obliczenia w tym, do czego się odwołujesz. Arytmetyka wartości stałych jest jednym z najprostszych i najbardziej oczywistych kompilatorów optymalizacji, więc za każdym razem, gdy zobaczysz wyrażenie, które ma tylko wartości stałe, możesz po prostu założyć, że całe wyrażenie jest zoptymalizowane do pojedynczej wartości stałej.
Jimmy Hoffa
@JimmyHoffa to prawda, ale z drugiej strony musi utworzyć 3 stałe zmienne przy każdym wywołaniu funkcji ...
Tomy
@ Stałe Tomy nie są zmiennymi. Nie różnią się, dlatego nie trzeba ich odtwarzać po kompilacji. Stała jest na ogół umieszczana w pamięci, a każde przyszłe sięgnięcie tej stałej jest kierowane dokładnie w to samo miejsce, nie ma potrzeby jej odtwarzania, ponieważ jej wartość nigdy się nie zmieni , dlatego nie jest zmienna. Kompilatory na ogół nie emitują kodu, który tworzy stałe, kompilator tworzy i kieruje wszystkie odwołania do kodu, co stworzył.
Jimmy Hoffa

Odpowiedzi:

23

Nowoczesne silniki JavaScript wykonują kompilację just-in-time. Nie możesz domniemywać, co „musi tworzyć w kółko”. W obu przypadkach tego rodzaju obliczenia są stosunkowo łatwe do optymalizacji.

Z drugiej strony, zamykanie stałych zmiennych nie jest typowym przypadkiem, dla którego celowałbyś w kompilację JIT. Zwykle tworzysz zamknięcie, gdy chcesz mieć możliwość zmiany tych zmiennych przy różnych wywołaniach. Tworzysz również dodatkowe dereferencje wskaźnika, aby uzyskać dostęp do tych zmiennych, na przykład różnicę między dostępem do zmiennej członka a lokalną int w OOP.

Tego rodzaju sytuacja powoduje, że ludzie odrzucają linię „przedwczesnej optymalizacji”. Łatwe optymalizacje są już wykonywane przez kompilator.

Karl Bielefeldt
źródło
Podejrzewam, że to wspomniane przejście przez zakres dla zmiennej rozdzielczości powoduje utratę. Wydaje się rozsądny, ale kto naprawdę wie, jakie szaleństwo tkwi w silniku JavaScript JIT ...
Jimmy Hoffa
1
Możliwe rozszerzenie tej odpowiedzi: powodem, dla którego JIT zignoruje optymalizację łatwą dla kompilatora offline, jest to, że wydajność całego kompilatora jest ważniejsza niż nietypowe przypadki.
Leushenko
12

Zmienne są tanie. Konteksty wykonania i łańcuchy zasięgu są drogie.

Istnieją różne odpowiedzi, które zasadniczo sprowadzają się do „ponieważ zamknięcia”, i są one w istocie prawdą, ale problem nie dotyczy konkretnie zamknięcia, to fakt, że masz funkcję odwołującą się do zmiennych w innym zakresie. windowMiałbyś ten sam problem, gdyby były to zmienne globalne na obiekcie, w przeciwieństwie do zmiennych lokalnych wewnątrz IIFE. Wypróbuj i przekonaj się.

Więc w pierwszej funkcji, gdy silnik zobaczy to zdanie:

var gbSize = size / GB;

Musi podjąć następujące kroki:

  1. Wyszukaj zmienną sizew bieżącym zakresie. (Znalazłem.)
  2. Wyszukaj zmienną GBw bieżącym zakresie. (Nie znaleziono.)
  3. Wyszukaj zmienną GBw zakresie nadrzędnym. (Znalazłem.)
  4. Wykonaj obliczenia i przypisz do gbSize.

Krok 3 jest znacznie droższy niż zwykła alokacja zmiennej. Co więcej, robisz to pięć razy , w tym dwa razy dla obu GBi MB. Podejrzewam, że jeśli aliowałeś je na początku funkcji (np. var gb = GB) I odwoływałeś się do aliasu, faktycznie spowodowałoby to niewielkie przyspieszenie, chociaż możliwe jest również, że niektóre silniki JS już przeprowadzają tę optymalizację. Oczywiście najskuteczniejszym sposobem na przyspieszenie wykonania jest po prostu wcale nie przechodzenie przez łańcuch zasięgu.

Należy pamiętać, że JavaScript nie jest jak skompilowany, statycznie typowany język, w którym kompilator rozwiązuje te zmienne adresy w czasie kompilacji. Silnik JS musi rozwiązać je według nazwy , a te wyszukiwania mają miejsce za każdym razem w środowisku wykonawczym. Więc chcesz ich unikać, kiedy to możliwe.

Przypisywanie zmiennych w JavaScript jest wyjątkowo tanie. To może być najtańsza operacja, chociaż nie mam nic na poparcie tego stwierdzenia. Niemniej jednak można śmiało powiedzieć, że prawie nigdy nie jest dobrym pomysłem unikanie tworzenia zmiennych; prawie każda optymalizacja, którą spróbujesz zrobić w tym obszarze, ostatecznie pogorszy sytuację, jeśli chodzi o wydajność.

Aaronaught
źródło
I nawet jeśli „optymalizacja” nie wpływa na wydajność negatywnie, to prawie na pewno jest wpłynie negatywnie na czytelność kodu. Co, chyba że robisz jakieś szalone obliczenia, jest najczęściej złym kompromisem (najwyraźniej niestety nie jest to bezpośrednie połączenie; wyszukaj „2009-02-17 11:41”). Jak podsumowuje: „Wybierz prędkość zamiast prędkości, jeśli prędkość nie jest absolutnie potrzebna”.
CVn
Nawet podczas pisania bardzo podstawowego interpretera dla języków dynamicznych dostęp do zmiennych w czasie wykonywania jest zwykle operacją O (1), a przejście w zakresie O (n) nie jest nawet potrzebne podczas początkowej kompilacji. W każdym zakresie do każdej nowo deklarowanej zmiennej przypisywany jest numer, więc biorąc pod uwagę, var a, b, cże możemy uzyskać dostęp bjako scope[1]. Wszystkie zakresy są ponumerowane, a jeśli zakres ten jest zagnieżdżony na pięciu zakresach, wówczas bjest w pełni adresowany, env[5][1]co jest znane podczas analizowania. W natywnym kodzie zakresy odpowiadają segmentom stosu. Zamknięcia są bardziej skomplikowane, gdyż muszą one wykonać kopię zapasową i zastąpićenv
Amon
@amon: To może być jak ci, że najlepiej jak go do pracy, ale to nie jest, jak to faktycznie działa. Ludzie o wiele bardziej kompetentni i doświadczeni niż ja pisałem o tym książki; w szczególności wskazałbym na wysokiej wydajności JavaScript autorstwa Nicholasa C. Zakasa. Oto krótki fragment , a on również porozmawiał z testami porównawczymi, aby go poprzeć. Oczywiście nie jest on jedyny, tylko najbardziej znany. JavaScript ma zakres leksykalny, więc zamknięcia naprawdę nie są tak wyjątkowe - w zasadzie wszystko jest zamknięciem.
Aaronaught
@Aaronaught Ciekawe. Ponieważ ta książka ma 5 lat, byłem zainteresowany, jak obecny silnik JS obsługuje zmienne wyszukiwania i spojrzał na backend silnika x8 silnika V8. Podczas analizy statycznej większość zmiennych jest rozwiązywana statycznie i ma przypisane przesunięcie pamięci w swoim zakresie. Zakresy funkcji są reprezentowane jako połączone listy, a zestaw jest emitowany jako rozwijana pętla, aby osiągnąć prawidłowy zakres. Tutaj dostalibyśmy ekwiwalent kodu C *(scope->outer + variable_offset)za dostęp; każdy poziom zakresu funkcji dodatkowej kosztuje jedno dodatkowe odchylenie wskaźnika. Wygląda na to, że oboje mieliśmy rację :)
amon
2

Jeden przykład dotyczy zamknięcia, drugi nie. Wdrożenie zamknięć jest dość trudne, ponieważ zmienne zamknięte nad zmiennymi nie działają jak zmienne normalne. Jest to bardziej oczywiste w języku niskiego poziomu, takim jak C, ale zilustruję to JavaScript.

Zamknięcie składa się nie tylko z funkcji, ale także ze wszystkich zmiennych, które zamknął. Kiedy chcemy wywołać tę funkcję, musimy również podać wszystkie zmienne zamknięte. Możemy modelować zamknięcie za pomocą funkcji, która otrzymuje obiekt jako pierwszy argument reprezentujący te zamknięte zmienne:

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

Zwróć uwagę na niezręczną konwencję wywoływania, która closure.apply(closure, ...realArgs)tego wymaga

Obsługa wbudowanych skryptów JavaScript umożliwia pominięcie jawnego varsargumentu i pozwala thiszamiast tego użyć :

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

Te przykłady są równoważne z tym kodem, który faktycznie używa zamknięć:

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

W tym ostatnim przykładzie obiekt służy tylko do grupowania dwóch zwróconych funkcji; thiswiązania nie ma znaczenia. Wszystkie szczegóły umożliwiające zamknięcie - przekazywanie ukrytych danych do faktycznej funkcji, zmiana wszystkich dostępów do zmiennych zamknięcia na wyszukiwanie w tych ukrytych danych - są obsługiwane przez język.

Ale wywoływanie zamknięć wiąże się z narzutem związanym z przekazywaniem tych dodatkowych danych, a uruchomienie zamknięcia wiąże się z narzutem związanym z wyszukiwaniem tych dodatkowych danych - pogarszanym przez złą lokalizację pamięci podręcznej i zwykle dereferencję wskaźnika w porównaniu ze zwykłymi zmiennymi - więc nie jest zaskakujące, że rozwiązanie, które nie opiera się na zamknięciach, działa lepiej. Zwłaszcza, że ​​wszystko, co oszczędza ci zamknięcie, to kilka wyjątkowo tanich operacji arytmetycznych, które mogą być nawet stale składane podczas analizowania.

amon
źródło