jak działa JavaScript: wewnątrz silnika V8 + 5 porad jak napisać zoptymalizowany kod

div>

kilka tygodni temu rozpoczęliśmy serię mającą na celu zagłębianie się w JavaScript i jego działanie: pomyśleliśmy, że znając elementy składowe JavaScript i sposób ich wspólnej gry, będziesz w stanie napisać lepszy kod i aplikacje.,

pierwszy post z serii skupił się na dostarczeniu przeglądu silnika, środowiska uruchomieniowego i stosu wywołań. Ten drugi post będzie nurkowanie do wewnętrznych części silnika JavaScript V8 Google. Przedstawimy również kilka szybkich wskazówek, jak napisać lepszy kod JavaScript —najlepsze praktyki nasz zespół programistów w SessionStack przestrzega podczas budowania produktu.

przegląd

silnik JavaScript to program lub interpreter wykonujący kod JavaScript., Silnik JavaScript może być zaimplementowany jako standardowy interpreter lub kompilator just-In-time, który kompiluje JavaScript do kodu bajtowego w jakiejś formie.,/li>

  • SpiderMonkey — pierwszy silnik JavaScript, który kiedyś napędzał Netscape Navigator, a dziś napędza Firefoksa
  • JavaScriptCore — open source, sprzedawany jako Nitro i rozwijany przez Apple dla Safari
  • KJS — silnik KDE pierwotnie opracowany przez Harri Portena dla projektu Konqueror przeglądarki KDE
  • Chakra (JScript9) — Internet Explorer
  • Chakra (JavaScript) — Microsoft Edge
  • Nashorn, open source jako część OpenJDK, napisany przez Oracle Java languages and Tool Group
  • jerryscript — jest lekkim silnikiem dla Internetu rzeczy.,
  • dlaczego powstał silnik V8?

    silnik V8 zbudowany przez Google jest open source i napisany w C++. Ten silnik jest używany wewnątrz Google Chrome. W przeciwieństwie do pozostałych silników, V8 jest również używany do popularnego węzła.js runtime.

    V8 został po raz pierwszy zaprojektowany w celu zwiększenia wydajności wykonywania JavaScript wewnątrz przeglądarek internetowych., W celu uzyskania szybkości, V8 tłumaczy kod JavaScript na bardziej wydajny kod maszynowy zamiast używać interpretera. Kompiluje kod JavaScript do kodu maszynowego podczas wykonywania przez implementację kompilatora JIT (Just-In-Time), jak wiele nowoczesnych silników JavaScript, takich jak SpiderMonkey lub Rhino (Mozilla). Główną różnicą jest to, że V8 nie tworzy bajtowego kodu ani żadnego pośredniego kodu.

    V8 miał dwa Kompilatory

    przed wersją 5.,9 z V8 wyszedł (wydany na początku tego roku), silnik wykorzystywał dwa Kompilatory:

    • full-codegen — prosty i bardzo szybki kompilator, który produkował prosty i stosunkowo wolny kod maszynowy.
    • wał korbowy — bardziej złożony (Just-In-Time) kompilator optymalizujący, który produkował wysoce zoptymalizowany kod.,l>
    • główny wątek robi to, czego byś się spodziewał: pobiera kod, kompiluje go, a następnie wykonuje
    • istnieje również oddzielny wątek do kompilacji, aby główny wątek mógł nadal wykonywać, podczas gdy ten pierwszy optymalizuje kod
    • wątek profilera, który powie runtime, na których metodach spędzamy dużo czasu, aby wał korbowy mógł je zoptymalizować
    • kilka wątków do obsługi Garbage Collector sweeps

    podczas pierwszego wykonywania kodu JavaScript, V8 wykorzystuje full-CodeGen, który bezpośrednio tłumaczy przetworzony JavaScript na kod maszynowy bez żadnej transformacji., Pozwala to na bardzo szybkie rozpoczęcie wykonywania kodu maszynowego. Zauważ, że V8 nie używa pośredniej reprezentacji kodu bajtowego w ten sposób, eliminując potrzebę interpretera.

    gdy twój kod działa przez jakiś czas, wątek profilera zebrał wystarczająco dużo danych, aby powiedzieć, która metoda powinna być zoptymalizowana.

    następnie optymalizacja wału korbowego rozpoczyna się w innym wątku. Tłumaczy abstrakcyjne drzewo składni JavaScript na statyczną reprezentację wysokiego poziomu (SSA) o nazwie Hydrogen i próbuje zoptymalizować ten wykres wodoru. Większość optymalizacji odbywa się na tym poziomie.,

    Inlining

    pierwsza optymalizacja polega na wstawieniu z góry jak największej ilości kodu. Inlining – proces polegający na zastąpieniu miejsca wywołania (linii kodu, w której wywoływana jest funkcja) ciałem wywołanej funkcji. Ten prosty krok pozwala na bardziej znaczące śledzenie optymalizacji.

    hidden class

    JavaScript jest językiem opartym na prototypach: nie ma klas i obiekty są tworzone przy użyciu procesu klonowania., JavaScript jest również dynamicznym językiem programowania, co oznacza, że właściwości mogą być łatwo dodawane lub usuwane z obiektu po jego utworzeniu.

    Większość interpreterów JavaScript używa struktur słownikowych (opartych na funkcji hash) do przechowywania lokalizacji wartości właściwości obiektu w pamięci. Struktura ta sprawia, że pobieranie wartości właściwości w JavaScript jest bardziej kosztowne obliczeniowo niż w nie dynamicznym języku programowania, takim jak Java czy C#., W Javie wszystkie właściwości obiektu są określone przez stały układ obiektu przed kompilacją i nie mogą być dynamicznie dodawane lub usuwane w czasie wykonywania (cóż, C# ma typ dynamic, który jest innym tematem). W rezultacie wartości właściwości (lub wskaźniki do tych właściwości) mogą być przechowywane w pamięci jako ciągły bufor ze stałym przesunięciem między każdą z nich. Długość przesunięcia może być łatwo określona na podstawie typu właściwości, podczas gdy nie jest to możliwe w JavaScript, gdzie typ właściwości może się zmieniać podczas wykonywania.,

    ponieważ używanie słowników do wyszukiwania właściwości obiektów w pamięci jest bardzo nieefektywne, V8 używa innej metody: ukrytych klas. Ukryte klasy działają podobnie do stałych układów obiektów (klas) używanych w językach takich jak Java, z tą różnicą, że są tworzone w czasie wykonywania. Teraz zobaczmy, jak one faktycznie wyglądają:

    function Point(x, y) {
    this.x = x;
    this.y = y;
    }var p1 = new Point(1, 2);

    gdy dojdzie do wywołania „new Point(1, 2)”, V8 utworzy ukrytą klasę o nazwie „C0”.,

    nie zdefiniowano jeszcze żadnych właściwości dla punktu, więc „C0” jest puste.

    Po raz pierwszy wypowiedź „to.X = x” jest wykonywany (wewnątrz funkcji „Point”), V8 utworzy drugą ukrytą klasę o nazwie „C1”, która jest oparta na „C0”. „C1″ opisuje miejsce w pamięci (względem wskaźnika obiektu), w którym znajduje się właściwość x., W tym przypadku,” x „jest przechowywane w offsecie 0, co oznacza, że podczas oglądania obiektu punktowego w pamięci jako bufor ciągły, pierwsze offset będzie odpowiadać właściwości”x”. V8 zaktualizuje również „C0 „z” class transition”, który stwierdza, że jeśli właściwość” x „zostanie dodana do obiektu punktowego, ukryta klasa powinna przełączyć się z” C0 „na”C1”. Ukrytą klasą dla obiektu punktowego poniżej jest teraz „C1”.,

    za każdym razem, gdy nowa właściwość dodana do obiektu Stara ukryta klasa jest aktualizowana ścieżką przejścia do nowej ukrytej klasy. Ukryte przejścia klas są ważne, ponieważ pozwalają na współdzielenie ukrytych klas między obiektami utworzonymi w ten sam sposób., Jeśli dwa obiekty dzielą ukrytą klasę i ta sama właściwość zostanie dodana do obu, przejścia zapewnią, że oba obiekty otrzymają tę samą nową ukrytą klasę i cały zoptymalizowany kod, który jest z nią związany.

    ten proces jest powtarzany, gdy polecenie „this.y = y” jest wykonywane (ponownie, wewnątrz funkcji punktowej, po ” this.X = X”).,

    Nowa ukryta Klasa o nazwie „C2” jest tworzona, Przejście klasy jest dodawane do „C1” stwierdzając, że jeśli właściwość „y” jest dodawana do obiektu punktowego (który zawiera już właściwość „x”), to ukryta klasa powinna zmienić się na „C2”, a ukryta Klasa obiektu punktowego zostanie zaktualizowana do „C2”.

    ukryte przejścia klas zależą od kolejności dodawania właściwości do obiektu., Spójrz na poniższy fragment kodu:

    function Point(x, y) {
    this.x = x;
    this.y = y;
    }var p1 = new Point(1, 2);
    p1.a = 5;
    p1.b = 6;var p2 = new Point(3, 4);
    p2.b = 7;
    p2.a = 8;

    teraz Można założyć, że zarówno dla p1, jak i p2 będą używane te same Ukryte klasy i przejścia. Nie bardzo. Dla „p1” najpierw zostanie dodana właściwość „a”, a następnie właściwość”b”. W przypadku „p2” przypisane jest jednak najpierw „b”, a następnie”a”. Tak więc, „p1″ i ” p2 ” kończą się różnymi ukrytymi klasami w wyniku różnych ścieżek przejścia. W takich przypadkach znacznie lepiej jest zainicjować właściwości dynamiczne w tej samej kolejności, aby Ukryte klasy mogły być ponownie użyte.,

    Inline caching

    V8 wykorzystuje inną technikę optymalizacji dynamicznie wpisywanych języków zwaną inline caching. Buforowanie w linii opiera się na obserwacji, że powtarzające się wywołania tej samej metody mają tendencję do występowania na tym samym typie obiektu. Szczegółowe wyjaśnienie buforowania inline można znaleźć tutaj.

    poruszymy ogólną koncepcję buforowania w linii (na wypadek, gdybyś nie miał czasu, aby przejść przez szczegółowe wyjaśnienie powyżej).

    Jak to działa?, V8 utrzymuje bufor typu obiektów, które zostały przekazane jako parametr w ostatnich wywołaniach metod i wykorzystuje te informacje do założenia typu obiektu, który będzie przekazany jako parametr w przyszłości. Jeśli V8 jest w stanie przyjąć dobre założenie co do typu obiektu, który zostanie przekazany do metody, może ominąć proces zastanawiania się, jak uzyskać dostęp do Właściwości obiektu, a zamiast tego użyć przechowywanych informacji z poprzednich wyszukiwań do ukrytej klasy obiektu.

    w jaki sposób koncepcje ukrytych klas i buforowania wbudowanego są ze sobą powiązane?, Ilekroć metoda jest wywoływana na określonym obiekcie, silnik V8 musi wykonać wyszukiwanie do ukrytej klasy tego obiektu w celu określenia przesunięcia dostępu do określonej właściwości. Po dwóch pomyślnych wywołaniach tej samej metody do tej samej ukrytej klasy, V8 pomija wyszukiwanie ukrytych klas i po prostu dodaje offset właściwości do samego wskaźnika obiektu. Dla wszystkich przyszłych wywołań tej metody, silnik V8 zakłada, że ukryta klasa nie zmieniła się i przeskakuje bezpośrednio do adresu pamięci dla określonej właściwości przy użyciu przesunięć przechowywanych z poprzednich wyszukiwań., To znacznie zwiększa szybkość wykonania.

    wbudowane buforowanie jest również powodem, dla którego tak ważne jest, aby obiekty tego samego typu współdzieliły Ukryte klasy. Jeśli utworzysz dwa obiekty tego samego typu i z różnymi ukrytymi klasami (jak to zrobiliśmy w przykładzie wcześniej), V8 nie będzie w stanie użyć buforowania w wierszu, ponieważ nawet jeśli oba obiekty są tego samego typu, ich odpowiadające im Ukryte klasy przypisują różne offsety do ich właściwości.,

    dwa obiekty są zasadniczo takie same ale właściwości „a” i „B” zostały utworzone w innej kolejności.

    Kompilacja do kodu maszynowego

    po zoptymalizowaniu wykresu wodoru wał korbowy obniża go do reprezentacji niższego poziomu zwanej litem. Większość implementacji litu jest specyficzna dla architektury. Przydział rejestru odbywa się na tym poziomie.,

    W końcu Lit jest kompilowany do kodu maszynowego. Potem dzieje się coś innego o nazwie OSR: wymiana na stosie. Zanim zaczęliśmy kompilować i optymalizować oczywiście długotrwałą metodę, prawdopodobnie ją uruchamialiśmy. V8 nie zapomni, co po prostu powoli wykonuje, aby zacząć od nowa z zoptymalizowaną wersją. Zamiast tego przekształci cały kontekst, który mamy (stos, rejestry), abyśmy mogli przełączyć się na zoptymalizowaną wersję w środku wykonania. Jest to bardzo złożone zadanie, mając na uwadze, że między innymi optymalizacje, V8 wstępnie wprowadził kod., V8 nie jest jedynym silnikiem zdolnym to zrobić.

    istnieją zabezpieczenia zwane deoptymizacją, aby dokonać odwrotnej transformacji i powrócić do nie zoptymalizowanego kodu w przypadku założenia, które silnik już nie jest prawdziwe.

    Garbage collection

    W przypadku garbage collection, V8 używa tradycyjnego generacyjnego podejścia mark-and-sweep do czyszczenia starej generacji. Faza znakowania ma zatrzymać wykonywanie JavaScript., Aby kontrolować koszty GC i uczynić wykonanie bardziej stabilnym, V8 używa znakowania przyrostowego: zamiast chodzić po całej stercie, próbując oznaczyć każdy możliwy obiekt, chodzi tylko część sterty, a następnie wznawia normalne wykonanie. Następny przystanek GC będzie kontynuowany od miejsca, w którym zatrzymał się poprzedni spacer sterty. Pozwala to na bardzo krótkie przerwy podczas normalnego wykonywania. Jak wspomniano wcześniej, Faza sweep jest obsługiwana przez oddzielne wątki.

    zapłon i TurboFan

    wraz z wydaniem V8 5.9 na początku 2017 roku wprowadzono nowy rurociąg wykonawczy., Ten nowy potok osiąga jeszcze większą poprawę wydajności i znaczne oszczędności pamięci w rzeczywistych aplikacjach JavaScript.

    nowy rurociąg uruchamiania jest zbudowany na bazie Ignition, interpretera V8 i TurboFan, najnowszego kompilatora optymalizującego V8.

    Możesz sprawdzić wpis na blogu zespołu V8 na ten temat tutaj.

    od wersji 5.,9 z V8 wyszedł, full-codegen i wał korbowy (technologie, które służyły V8 od 2010) nie są już używane przez V8 do wykonywania JavaScript jak zespół V8 starała się nadążyć za nowymi funkcjami języka JavaScript i optymalizacje potrzebne do tych funkcji.

    oznacza to, że ogólnie V8 będzie miał znacznie prostszą i łatwiejszą do utrzymania architekturę.

    ulepszenia w sieci i węzłach.,js benchmarki

    te ulepszenia to dopiero początek. Nowy Ignition i TurboFan pipeline torują drogę do dalszych optymalizacji, które zwiększą wydajność JavaScript i zmniejszą ślad V8 zarówno w Chrome, jak i Node.js w nadchodzących latach.

    na koniec kilka porad i wskazówek, jak pisać dobrze zoptymalizowany, lepszy JavaScript., Możesz łatwo czerpać je z powyższej treści, jednak dla Twojej wygody, oto podsumowanie:

    Jak napisać zoptymalizowany JavaScript

    1. kolejność właściwości obiektu: zawsze tworzy instancje właściwości obiektu w tej samej kolejności, aby Ukryte klasy, a następnie zoptymalizowany kod, mogły być współdzielone.
    2. właściwości dynamiczne: dodanie właściwości do obiektu po utworzeniu instancji wymusi zmianę ukrytej klasy i spowolni wszelkie metody zoptymalizowane dla poprzedniej ukrytej klasy. Zamiast tego Przypisz wszystkie właściwości obiektu do konstruktora.,
    3. metody: kod, który wykonuje tę samą metodę wielokrotnie, będzie działał szybciej niż kod, który wykonuje wiele różnych metod tylko raz(z powodu wbudowanego buforowania).
    4. Tablice: unikaj rzadkich tablic, w których klucze nie są liczbami przyrostowymi. Rzadkie tablice, które nie mają w sobie każdego elementu, są tabelą hash. Elementy w takich tablicach są droższe w dostępie. Staraj się również unikać wstępnego przydzielania dużych tablic. Lepiej rośnie w miarę rozwoju. Na koniec nie usuwaj elementów z tablic. To sprawia, że klucze są rzadkie.
    5. otagowane wartości: V8 przedstawia obiekty i liczby z 32 bitami., Używa bitu, aby dowiedzieć się, czy jest to obiekt (flag = 1) Czy liczba całkowita (flag = 0) o nazwie smi (SMall Integer) ze względu na jego 31 bitów. Następnie, jeśli wartość liczbowa jest większa niż 31 bitów, V8 poleci liczbę, zamieniając ją na podwójną i tworząc nowy obiekt, aby umieścić liczbę w środku. Staraj się używać 31-bitowych podpisanych liczb w miarę możliwości, aby uniknąć kosztownej operacji bokserskiej w obiekcie JS.

    W SessionStack staramy się przestrzegać tych najlepszych praktyk w pisaniu wysoce zoptymalizowanego kodu JavaScript., Powodem jest to, że po zintegrowaniu SessionStack z aplikacją produkcyjną zaczyna ona nagrywać wszystko: wszystkie zmiany DOM, interakcje użytkowników, wyjątki JavaScript, ślady stosu, nieudane żądania sieciowe i wiadomości debugowania.
    dzięki SessionStack możesz odtworzyć problemy w swoich aplikacjach internetowych jako Filmy i zobaczyć wszystko, co przydarzyło się twojemu użytkownikowi. A wszystko to musi się zdarzyć bez wpływu na wydajność Twojej aplikacji internetowej.
    istnieje darmowy plan, który pozwala na rozpoczęcie pracy za darmo.

    Share

    Dodaj komentarz

    Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *