Czy korzystanie z funkcji anonimowych wpływa na wydajność?


89

Zastanawiałem się, czy istnieje różnica w wydajności między używaniem funkcji nazwanych a funkcjami anonimowymi w JavaScript?

for (var i = 0; i < 1000; ++i) {
    myObjects[i].onMyEvent = function() {
        // do something
    };
}

vs

function myEventHandler() {
    // do something
}

for (var i = 0; i < 1000; ++i) {
    myObjects[i].onMyEvent = myEventHandler;
}

Pierwsza jest bardziej uporządkowana, ponieważ nie zaśmieca twojego kodu rzadko używanymi funkcjami, ale czy ma znaczenie, że deklarujesz tę funkcję wiele razy?


Wiem, że nie chodzi o to, ale jeśli chodzi o czystość / czytelność kodu, myślę, że „właściwa droga” jest gdzieś pośrodku. „Bałagan” rzadko używanych funkcji najwyższego poziomu jest denerwujący, ale tak samo jest z mocno zagnieżdżonym kodem, który w dużej mierze zależy od anonimowych funkcji, które są zadeklarowane w linii z ich wywołaniem (pomyślmy o piekle wywołań zwrotnych node.js). Zarówno pierwszy, jak i drugi mogą utrudniać debugowanie / śledzenie wykonania.
Zac B

Poniższe testy wydajności uruchamiają funkcję dla tysięcy iteracji. Nawet jeśli zauważysz istotną różnicę, większość przypadków użycia nie będzie tego robić w iteracjach tej kolejności. Dlatego lepiej wybrać to, co odpowiada Twoim potrzebom i zignorować wydajność w tym konkretnym przypadku.
użytkownik

@nickf oczywiście jest to zbyt stare pytanie, ale zobacz nową zaktualizowaną odpowiedź
Chandan Pasunoori

Odpowiedzi:


89

Problem z wydajnością jest tutaj kosztem tworzenia nowego obiektu funkcji przy każdej iteracji pętli, a nie faktem, że używasz funkcji anonimowej:

for (var i = 0; i < 1000; ++i) {    
    myObjects[i].onMyEvent = function() {
        // do something    
    };
}

Tworzysz tysiąc różnych obiektów funkcji, mimo że mają ten sam kod i nie mają powiązania z zakresem leksykalnym ( zamknięcie ). Z drugiej strony, poniższe wydaje się szybsze, ponieważ po prostu przypisuje to samo odwołanie do funkcji do elementów tablicy w całej pętli:

function myEventHandler() {
    // do something
}

for (var i = 0; i < 1000; ++i) {
    myObjects[i].onMyEvent = myEventHandler;
}

Jeśli miałbyś utworzyć anonimową funkcję przed wejściem do pętli, a następnie przypisać do niej odwołania do elementów tablicy tylko wewnątrz pętli, zauważysz, że nie ma żadnej różnicy wydajności lub semantycznej w porównaniu z wersją funkcji o nazwie:

var handler = function() {
    // do something    
};
for (var i = 0; i < 1000; ++i) {    
    myObjects[i].onMyEvent = handler;
}

Krótko mówiąc, korzystanie z funkcji anonimowych zamiast nazwanych nie wiąże się z żadnymi zauważalnymi kosztami wydajności.

Nawiasem mówiąc, z góry może się wydawać, że nie ma różnicy między:

function myEventHandler() { /* ... */ }

i:

var myEventHandler = function() { /* ... */ }

Pierwsza jest deklaracją funkcji, podczas gdy druga jest przypisaniem zmiennej do funkcji anonimowej. Chociaż może się wydawać, że mają ten sam efekt, JavaScript traktuje je nieco inaczej. Aby zrozumieć różnicę, polecam przeczytanie „ Niejednoznaczności deklaracji funkcji JavaScript ”.

Rzeczywisty czas wykonania dowolnego podejścia będzie w dużej mierze podyktowany implementacją kompilatora i środowiska wykonawczego w przeglądarce. Aby uzyskać pełne porównanie wydajności współczesnych przeglądarek, odwiedź witrynę JS Perf


Zapomniałeś o nawiasach przed treścią funkcji. Właśnie to przetestowałem, są wymagane.
Chinoto Vokro,

wydaje się, że wyniki benchmarków są bardzo zależne od silnika js!
aleclofabbro

3
Czy nie ma błędu w przykładzie Perf JS: Przypadek 1 definiuje tylko funkcję, podczas gdy przypadek 2 i 3 wydaje się przypadkowo wywoływać funkcję.
bluenote10

Czy używając tego rozumowania, oznacza to, że podczas tworzenia node.jsaplikacji internetowych lepiej jest tworzyć funkcje poza przepływem żądań i przekazywać je jako wywołania zwrotne, niż tworzyć anonimowe wywołania zwrotne?
Xavier T Mukodi

23

Oto mój kod testowy:

var dummyVar;
function test1() {
    for (var i = 0; i < 1000000; ++i) {
        dummyVar = myFunc;
    }
}

function test2() {
    for (var i = 0; i < 1000000; ++i) {
        dummyVar = function() {
            var x = 0;
            x++;
        };
    }
}

function myFunc() {
    var x = 0;
    x++;
}

document.onclick = function() {
    var start = new Date();
    test1();
    var mid = new Date();
    test2();
    var end = new Date();
    alert ("Test 1: " + (mid - start) + "\n Test 2: " + (end - mid));
}

Wyniki:
Test 1: 142 ms Test 2: 1983 ms

Wygląda na to, że silnik JS nie rozpoznaje, że jest to ta sama funkcja w Test2 i kompiluje ją za każdym razem.


3
W której przeglądarce przeprowadzono ten test?
andynil

5
Czasy dla mnie w Chrome 23: (2 ms / 17 ms), IE9: (20 ms / 83 ms), FF 17: (2 ms / 96 ms)
Davy8

Twoja odpowiedź zasługuje na większą wagę. Moje czasy na Intel i5 4570S: Chrome 41 (1/9), IE11 (1/25), FF36 (1/14). Oczywiście anonimowa funkcja w pętli działa gorzej.
ThisClark

3
Ten test nie jest tak przydatny, jak się wydaje. W żadnym z przykładów funkcja wewnętrzna nie jest faktycznie wykonywana. W rzeczywistości cały ten test pokazuje, że tworzenie funkcji 10000000 razy jest szybsze niż jednokrotne tworzenie funkcji.
Nucleon,

2

Zgodnie z ogólną zasadą projektowania, należy unikać wielokrotnego implementowania tego samego kodu. Zamiast tego powinieneś podnieść wspólny kod do funkcji i wykonać tę (ogólną, dobrze przetestowaną, łatwą do modyfikacji) funkcję z wielu miejsc.

Jeśli (w przeciwieństwie do tego, co wnioskujesz ze swojego pytania) raz deklarujesz funkcję wewnętrzną i raz używasz tego kodu (i nie masz nic identycznego w swoim programie), to prawdopodobnie funkcja anonomiczna (to jest przypuszczenie) zostanie potraktowana w ten sam sposób przez kompilator jako normalną nazwaną funkcję.

Jest to bardzo przydatna funkcja w określonych przypadkach, ale nie powinna być używana w wielu sytuacjach.


1

Nie spodziewałbym się dużej różnicy, ale jeśli istnieje, prawdopodobnie będzie się różnić w zależności od silnika skryptowego lub przeglądarki.

Jeśli okaże się, że kod jest łatwiejszy do zrozumienia, wydajność nie stanowi problemu, chyba że spodziewasz się wywoływać funkcję miliony razy.


1

Tam, gdzie możemy mieć wpływ na wydajność, jest operacja deklarowania funkcji. Oto wzorzec deklarowania funkcji w kontekście innej funkcji lub na zewnątrz:

http://jsperf.com/function-context-benchmark

W Chrome operacja przebiega szybciej, jeśli zadeklarujemy funkcję na zewnątrz, ale w Firefoksie jest odwrotnie.

W innym przykładzie widzimy, że jeśli funkcja wewnętrzna nie jest czystą funkcją, będzie miała brak wydajności również w przeglądarce Firefox: http://jsperf.com/function-context-benchmark-3


0

To, co zdecydowanie przyspieszy twoją pętlę w różnych przeglądarkach, zwłaszcza przeglądarkach IE, zapętla się w następujący sposób:

for (var i = 0, iLength = imgs.length; i < iLength; i++)
{
   // do something
}

Podałeś dowolne 1000 w warunku pętli, ale otrzymujesz mój dryf, jeśli chcesz przejrzeć wszystkie elementy w tablicy.


0

odniesienie prawie zawsze będzie wolniejsze niż rzecz, do której się odnosi. Pomyśl o tym w ten sposób - powiedzmy, że chcesz wydrukować wynik dodania 1 + 1. Co ma większy sens:

alert(1 + 1);

lub

a = 1;
b = 1;
alert(a + b);

Zdaję sobie sprawę, że to naprawdę uproszczony sposób patrzenia na to, ale jest to przykładowe, prawda? Używaj odwołania tylko wtedy, gdy będzie używane wiele razy - na przykład, który z tych przykładów ma większy sens:

$(a.button1).click(function(){alert('you clicked ' + this);});
$(a.button2).click(function(){alert('you clicked ' + this);});

lub

function buttonClickHandler(){alert('you clicked ' + this);}
$(a.button1).click(buttonClickHandler);
$(a.button2).click(buttonClickHandler);

Druga to lepsza praktyka, nawet jeśli ma więcej linii. Mam nadzieję, że to wszystko jest pomocne. (a składnia jquery nikogo nie zrzuciła)


0

@nickf

(szkoda, że ​​nie mam przedstawiciela do komentowania, ale dopiero co znalazłem tę witrynę)

Chodzi mi o to, że istnieje zamieszanie między nazwanymi / anonimowymi funkcjami a przypadkiem użycia wykonywania + kompilacji w iteracji. Jak zilustrowałem, różnica między anon + named jest sama w sobie pomijalna - mówię, że to przypadek użycia jest wadliwy.

Wydaje mi się to oczywiste, ale jeśli nie, myślę, że najlepszą radą jest „nie rób głupich rzeczy” (jednym z nich jest ciągłe przesuwanie bloków + tworzenie obiektów w tym przypadku użycia), a jeśli nie jesteś pewien, przetestuj!



0

Obiekty anonimowe są szybsze niż obiekty nazwane. Jednak wywoływanie większej liczby funkcji jest droższe i w stopniu, który przyćmiewa wszelkie oszczędności, jakie można uzyskać dzięki korzystaniu z funkcji anonimowych. Każda wywoływana funkcja dodaje do stosu wywołań, co wprowadza niewielką, ale nietrywialną ilość narzutu.

Ale jeśli nie piszesz procedur szyfrowania / deszyfrowania lub czegoś podobnie wrażliwego na wydajność, jak zauważyło wielu innych, zawsze lepiej jest zoptymalizować pod kątem eleganckiego, łatwego do odczytania kodu niż szybki kod.

Zakładając, że piszesz dobrze zaprojektowany kod, za kwestie szybkości powinny odpowiadać osoby piszące interpretery / kompilatory.


0

@nickf

To raczej głupi test, porównujesz tam czas wykonania i kompilacji, co oczywiście będzie kosztować metodę 1 (kompiluje N razy, w zależności od silnika JS) z metodą 2 (kompiluje się raz). Nie wyobrażam sobie programisty JS, który zdałby swój okres próbny, pisząc kod w taki sposób.

O wiele bardziej realistycznym podejściem jest przypisanie anonimowe, ponieważ w rzeczywistości używasz do dokumentu. Metoda onclick jest bardziej podobna do poniższej, która w rzeczywistości łagodnie faworyzuje metodę anon.

Używając podobnej struktury testowej do twojej:


function test(m)
{
    for (var i = 0; i < 1000000; ++i) 
    {
        m();
    }
}

function named() {var x = 0; x++;}

var test1 = named;

var test2 = function() {var x = 0; x++;}

document.onclick = function() {
    var start = new Date();
    test(test1);
    var mid = new Date();
    test(test2);
    var end = new Date();
    alert ("Test 1: " + (mid - start) + "ms\n Test 2: " + (end - mid) + "ms");
}

0

Jak wskazano w komentarzach do @nickf odpowiedź: odpowiedź na

Tworzy funkcję raz szybciej niż tworzy ją milion razy

jest po prostu tak. Ale jak pokazuje jego perf w JS, nie jest wolniejszy nawet milion razy, co pokazuje, że z czasem staje się coraz szybszy.

Bardziej interesujące dla mnie pytanie brzmi:

W jaki sposób powtarzane tworzenie + uruchamianie porównuje się z tworzeniem jednokrotnego + powtarzanego przebiegu .

Jeśli funkcja wykonuje złożone obliczenia, czas potrzebny na utworzenie obiektu funkcji jest najprawdopodobniej pomijalny. Ale co z nad głową tworzenia w przypadkach, gdy bieg jest szybki? Na przykład:

// Variant 1: create once
function adder(a, b) {
  return a + b;
}
for (var i = 0; i < 100000; ++i) {
  var x = adder(412, 123);
}

// Variant 2: repeated creation via function statement
for (var i = 0; i < 100000; ++i) {
  function adder(a, b) {
    return a + b;
  }
  var x = adder(412, 123);
}

// Variant 3: repeated creation via function expression
for (var i = 0; i < 100000; ++i) {
  var x = (function(a, b) { return a + b; })(412, 123);
}

Ten JS Perf pokazuje, że jednokrotne utworzenie funkcji jest szybsze zgodnie z oczekiwaniami. Jednak nawet przy bardzo szybkiej operacji, takiej jak proste dodawanie, narzut związany z wielokrotnym tworzeniem funkcji wynosi tylko kilka procent.

Różnica prawdopodobnie stanie się znacząca tylko w przypadkach, gdy tworzenie obiektu funkcji jest złożone, przy zachowaniu znikomego czasu wykonywania, np. Jeśli cała treść funkcji jest opakowana w plik if (unlikelyCondition) { ... }.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.