Wpływ funkcji const, let i var na wydajność JavaScript v8?

88

Niezależnie od różnic funkcjonalnych, czy użycie nowych słów kluczowych „let” i „const” ma jakikolwiek ogólny lub konkretny wpływ na wydajność w stosunku do „var”?

Po uruchomieniu programu:

function timeit(f, N, S) {
    var start, timeTaken;
    var stats = {min: 1e50, max: 0, N: 0, sum: 0, sqsum: 0};
    var i;
    for (i = 0; i < S; ++i) {
        start = Date.now();
        f(N);
        timeTaken = Date.now() - start;

        stats.min = Math.min(timeTaken, stats.min);
        stats.max = Math.max(timeTaken, stats.max);
        stats.sum += timeTaken;
        stats.sqsum += timeTaken * timeTaken;
        stats.N++
    }

    var mean = stats.sum / stats.N;
    var sqmean = stats.sqsum / stats.N;

    return {min: stats.min, max: stats.max, mean: mean, spread: Math.sqrt(sqmean - mean * mean)};
}

var variable1 = 10;
var variable2 = 10;
var variable3 = 10;
var variable4 = 10;
var variable5 = 10;
var variable6 = 10;
var variable7 = 10;
var variable8 = 10;
var variable9 = 10;
var variable10 = 10;

function varAccess(N) {
    var i, sum;
    for (i = 0; i < N; ++i) {
        sum += variable1;
        sum += variable2;
        sum += variable3;
        sum += variable4;
        sum += variable5;
        sum += variable6;
        sum += variable7;
        sum += variable8;
        sum += variable9;
        sum += variable10;
    }
    return sum;
}

const constant1 = 10;
const constant2 = 10;
const constant3 = 10;
const constant4 = 10;
const constant5 = 10;
const constant6 = 10;
const constant7 = 10;
const constant8 = 10;
const constant9 = 10;
const constant10 = 10;

function constAccess(N) {
    var i, sum;
    for (i = 0; i < N; ++i) {
        sum += constant1;
        sum += constant2;
        sum += constant3;
        sum += constant4;
        sum += constant5;
        sum += constant6;
        sum += constant7;
        sum += constant8;
        sum += constant9;
        sum += constant10;
    }
    return sum;
}


function control(N) {
    var i, sum;
    for (i = 0; i < N; ++i) {
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
    }
    return sum;
}

console.log("ctl = " + JSON.stringify(timeit(control, 10000000, 50)));
console.log("con = " + JSON.stringify(timeit(constAccess, 10000000, 50)));
console.log("var = " + JSON.stringify(timeit(varAccess, 10000000, 50)));

.. Moje wyniki były następujące:

ctl = {"min":101,"max":117,"mean":108.34,"spread":4.145407097016924}
con = {"min":107,"max":572,"mean":435.7,"spread":169.4998820058587}
var = {"min":103,"max":608,"mean":439.82,"spread":176.44417700791374}

Jednak dyskusja, jak tu wspomniano, wydaje się wskazywać na rzeczywisty potencjał różnic w wydajności w pewnych scenariuszach: https://esdiscuss.org/topic/performance-concern-with-let-const

sean2078
źródło
Myślę, że zależy to od zastosowania, na przykład letużycie w zakresie blokowym powinno być bardziej wydajne niż var, które nie ma zakresu blokowego, ale tylko zakres funkcji.
adeneo
Jeśli mogę zapytać, dlaczego to @adeneo?
sean2078
1
@ sean2078 - jeśli chcesz zadeklarować zmienną, która żyje tylko w zakresie blokowym, letzrobiłaby to, a następnie została zebrana jako śmieci, podczas gdy var, która jest objęta zakresem funkcji, nie musi działać w ten sam sposób. Znowu myślę, że jest to tak specyficzne dla zastosowania, że ​​jedno leti drugie const może być bardziej wydajne, ale nie zawsze będzie.
adeneo
1
Jestem zdezorientowany tym, w jaki sposób cytowany kod ma zademonstrować różnicę między vari let: W ogóle nie używa let.
TJ Crowder
1
Obecnie nie jest - tylko const vs. var .. Pierwotnie pochodzi z gist.github.com/srikumarks/1431640 (kredyt dla srikumarks), jednak poproszono o wciągnięcie kodu w wątpliwość
sean2078

Odpowiedzi:

116

TL; DR

W teorii niezoptymalizowana wersja tej pętli:

for (let i = 0; i < 500; ++i) {
    doSomethingWith(i);
}

może być wolniejsze niż niezoptymalizowana wersja tej samej pętli z var:

for (var i = 0; i < 500; ++i) {
    doSomethingWith(i);
}

ponieważ dla każdej iteracji pętli tworzona jest inna i zmienna let, podczas gdy jest tylko jedna zmienna iz var.

Argumentując przeciwko temu , varjest podniesiony, więc jest deklarowany poza pętlą, podczas gdy letjest deklarowany tylko w pętli, co może dać zaletę optymalizacji.

W praktyce w 2018 roku nowoczesne silniki JavaScript przeprowadzają wystarczająco dużo introspekcji pętli, aby wiedzieć, kiedy mogą zoptymalizować tę różnicę. (Nawet wcześniej istnieje prawdopodobieństwo, że pętla wykonywała wystarczająco dużo pracy, aby i tak zlikwidować dodatkowe letobciążenie. Ale teraz nie musisz się tym nawet martwić).

Uważaj na syntetyczne testy porównawcze, ponieważ są one niezwykle łatwe do popełnienia i uruchamiają optymalizatory silnika JavaScript w sposób, w jaki nie ma tego prawdziwy kod (zarówno dobry, jak i zły). Jeśli jednak chcesz syntetycznego testu porównawczego, oto jeden:

Mówi, że nie ma znaczącej różnicy w tym syntetycznym teście na V8 / Chrome lub SpiderMonkey / Firefox. (W powtórzonych testach w obu przeglądarkach jedna wygrywa, a druga wygrywa, w obu przypadkach z marginesem błędu). Ale znowu jest to syntetyczny test porównawczy, a nie twój kod. Martw się o wydajność kodu, kiedy i czy w kodzie występuje problem z wydajnością.

Ze względu na styl, wolę letkorzyści związane ze skalowaniem i zamknięciem w pętlach, jeśli używam zmiennej pętli w zamknięciu.

Detale

Istotna różnica między pętlą vari letw forpętli polega na tym, że idla każdej iteracji tworzone jest inne ; rozwiązuje klasyczny problem „zamknięć w pętli”:

Tworzenie nowego rejestru EnvironmentRecord dla każdej treści pętli ( łącza specyfikacji ) jest pracochłonne i wymaga czasu, dlatego teoretycznie letwersja jest wolniejsza niż varwersja.

Ale różnica ma znaczenie tylko wtedy, gdy utworzysz funkcję (zamknięcie) w pętli, która używa i, tak jak to zrobiłem w powyższym przykładzie wykonywalnego fragmentu. W przeciwnym razie tego rozróżnienia nie można zaobserwować i można je zoptymalizować.

Tutaj w 2018 roku wygląda na to, że V8 (i SpiderMonkey w Firefoksie) przeprowadza wystarczającą introspekcję, aby w pętli nie było kosztów wydajnościowych, które nie wykorzystują letsemantyki zmiennej na iterację. Zobacz ten test .


W niektórych przypadkach constmoże stanowić okazję do optymalizacji, która varby się nie udała, zwłaszcza w przypadku zmiennych globalnych.

Problem ze zmienną globalną polega na tym, że jest ona globalna; każdy kod w dowolnym miejscu mógłby uzyskać do niego dostęp. Więc jeśli zadeklarujesz zmienną var, której nigdy nie zamierzasz zmieniać (i nigdy nie zmieniasz kodu), silnik nie może założyć, że nigdy się nie zmieni w wyniku kodu załadowanego później lub w podobny sposób.

Dzięki const, choć jesteś wyraźnie mówi, że silnik nie może change¹ wartość. Dlatego można dowolnie przeprowadzać optymalizację, w tym emitować literał zamiast odniesienia do zmiennej do kodu, który go używa, wiedząc, że wartości nie można zmienić.

¹ Pamiętaj, że w przypadku obiektów wartość jest odniesieniem do obiektu, a nie samym obiektem. Tak więc const o = {}, możesz zmienić stan obiektu ( o.answer = 42), ale nie możesz owskazać nowego obiektu (ponieważ wymagałoby to zmiany odniesienia do obiektu, który zawiera).


Podczas używania letlub constw innych varsytuacjach podobnych, prawdopodobnie nie będą miały innej wydajności. Ta funkcja powinna mieć dokładnie taką samą wydajność, niezależnie od tego, czy używasz, varczy letna przykład:

function foo() {
    var i = 0;
    while (Math.random() < 0.5) {
        ++i;
    }
    return i;
}

To wszystko oczywiście nie będzie miało znaczenia i jest czymś, czym należy się martwić tylko wtedy, gdy pojawi się prawdziwy problem do rozwiązania.

TJ Crowder
źródło
Dzięki za odpowiedź - zgadzam się, więc dla siebie ustandaryzowałem używanie var do operacji zapętlania, jak wspomniano w twoim pierwszym przykładzie pętli for, i niech / const dla wszystkich innych deklaracji, zakładając, że różnica w wydajności w zasadzie nie istnieje, jak wydaje się test wydajności na razie wskazać. Być może później zostaną dodane optymalizacje dotyczące const. To znaczy, chyba że ktoś inny może wykazać zauważalną różnicę za pomocą przykładu kodu.
sean2078
@ sean2078: Używam również letw przykładzie pętli. Różnica w wydajności po prostu nie jest warta martwienia się o to w przypadku 99,999%.
TJ Crowder
2
Od połowy 2018 roku wersje z let i var mają tę samą prędkość w Chrome, więc teraz nie ma już różnicy.
Maksymalnie
1
@DanM .: Dobra wiadomość, wydaje się, że optymalizacja nadrobiła zaległości, przynajmniej w V8 i SpiderMonkey. :-)
TJ Crowder
1
Dzięki. Słusznie.
Hypers
18

„LET” JEST LEPSZE W DEKLARACJACH PĘTLI

Za pomocą prostego testu (5 razy) w nawigatorze:

// WITH VAR
console.time("var-time")
for(var i = 0; i < 500000; i++){}
console.timeEnd("var-time")

Średni czas wykonania to ponad 2,5 ms

// WITH LET
console.time("let-time")
for(let i = 0; i < 500000; i++){}
console.timeEnd("let-time")

Średni czas wykonania to ponad 1,5 ms

Odkryłem, że czas pętli z let jest lepszy.

Amn
źródło
6
Uruchamiając to w przeglądarce Firefox 65.0, uzyskałem średnie prędkości var=138.8msi let=4ms. To nie jest literówka, letjest teraz ponad 30 razy szybszy
Katamari,
6
Właśnie przetestowałem to w Node 12.5. Okazało się, że średnie prędkości to var=2.6msi let=1.0ms. Więc wpuść Node jest nieco ponad dwa razy szybszy.
Kane Hooper,
2
Aby zaznaczyć, że testowanie wydajności jest trudne w obecności optymalizatorów: myślę, że pętla let jest całkowicie optymalizowana - niech istnieje tylko wewnątrz bloku, a pętla nie ma skutków ubocznych, a V8 jest wystarczająco inteligentny, aby wiedzieć, że może po prostu usuń blok, a następnie pętlę. deklaracja var jest podnoszona, więc nie może tego wiedzieć. Twoje pętle takie, jakie są, otrzymuję 1 ms / 0,4 ms, jednak jeśli dla obu mam zmienną j (var lub let) poza pętlą, która również jest zwiększana, otrzymuję 1 ms / 1,5 ms. tj. var loop bez zmian, niech pętla trwa teraz dłużej.
Euan Smith
@KaneHooper - Jeśli masz pięciokrotną różnicę w Firefoksie, to musiała to być pusta pętla, która to zrobiła. Prawdziwe pętle nie mają pustych ciał.
TJ Crowder
Uważaj na syntetyczne benchmarki , a zwłaszcza te z pętlami z pustymi ciałami. Jeśli faktycznie robisz coś w pętli, ten syntetyczny test porównawczy (którego znowu uważaj! :-)) sugeruje, że nie ma znaczącej różnicy. Dodałem też jeden do mojej odpowiedzi, więc jest na miejscu (nie tak jak te testy jsPerf, które znikały na mnie. :-)). Powtarzane biegi pokazują, że jedna wygrywa, a druga wygrywa. Na pewno nic rozstrzygającego.
TJ Crowder
8

Odpowiedź TJ Crowdera jest doskonała.

Oto dodatek: „Kiedy uzyskam największy zwrot z każdej zainwestowanej złotówki przy edycji istniejących deklaracji var na const?”

Zauważyłem, że największy wzrost wydajności miał związek z funkcjami „eksportowanymi”.

Więc jeśli pliki A, B, R i Z wywołują funkcję „narzędzia” w pliku U, która jest powszechnie używana w Twojej aplikacji, przełączenie tej funkcji narzędzia na „const” i odniesienie do pliku nadrzędnego na stałą może eak trochę lepszej wydajności. Wydawało mi się, że nie jest to mierzalnie szybsze, ale ogólne zużycie pamięci zostało zmniejszone o około 1-3% w przypadku mojej rażąco monolitycznej aplikacji Frankenstein-ed. Co, jeśli wydajesz mnóstwo gotówki w chmurze lub na serwerze baremetal, może być dobrym powodem, aby poświęcić 30 minut na przeszukanie i zaktualizowanie niektórych z tych deklaracji var do const.

Zdaję sobie sprawę, że jeśli przeczytasz jak const, var i pozwolisz pracować pod okładkami, prawdopodobnie już doszedłeś do powyższego ... ale na wypadek, gdybyś "spojrzał" na to: D.

Z tego, co pamiętam z testów porównawczych w węźle v8.12.0, kiedy wykonywałem aktualizację, moja aplikacja przeszła z bezczynnego zużycia ~ 240 MB RAM do ~ 233 MB RAM.

isaacdre
źródło
2

Odpowiedź TJ Crowdera jest bardzo dobra, ale:

  1. „let” ma na celu uczynienie kodu bardziej czytelnym, a nie bardziej wydajnym
  2. w teorii niech będzie wolniejsze niż var
  3. w praktyce kompilator nie może całkowicie rozwiązać (analiza statyczna) nieukończonego programu, więc czasami pominie optymalizację
  4. w każdym przypadku użycie „let” będzie wymagało więcej procesora do introspekcji, ławka musi zostać uruchomiona, gdy Google v8 zacznie analizować
  5. jeśli introspekcja się nie powiedzie, „niech” będzie mocno naciskać na moduł odśmiecania pamięci V8, jego zwolnienie / ponowne użycie będzie wymagało większej liczby iteracji. będzie również zużywać więcej pamięci RAM. ławka musi uwzględnić te punkty
  6. Zamknięcie Google zmieni niech w var ...

Efekt luki w wydajności między var i let można zobaczyć w prawdziwym, kompletnym programie, a nie w pojedynczej podstawowej pętli.

W każdym razie użycie let tam, gdzie nie musisz, sprawia, że ​​twój kod jest mniej czytelny.

Michael Valve
źródło