Deweloper V8 tutaj. Biorąc pod uwagę zainteresowanie tym pytaniem i brak innych odpowiedzi, mogę spróbować; Obawiam się, że nie będzie to odpowiedź, na którą liczyłeś.
Czy jest gdzieś zbiór wskazówek, jak programować, pozostając w świecie zapakowanych tablic SMI (na przykład)?
Krótka odpowiedź: to właśnie tu: const guidelines = ["keep your integers small enough"]
.
Dłuższa odpowiedź: udzielenie kompleksowego zestawu wytycznych jest trudne z różnych powodów. Ogólnie rzecz biorąc, naszym zdaniem jest to, że programiści JavaScript powinni pisać kod, który ma dla nich sens i ich zastosowanie, a programiści silnika JavaScript powinni dowiedzieć się, jak szybko uruchomić ten kod na swoich silnikach. Z drugiej strony, istnieją oczywiście pewne ograniczenia tego ideału, w tym sensie, że niektóre wzorce kodowania zawsze będą miały wyższe koszty wydajności niż inne, niezależnie od wyborów implementacji silnika i działań optymalizacyjnych.
Kiedy mówimy o zaleceniach dotyczących wydajności, staramy się o tym pamiętać i dokładnie oszacować, które rekomendacje mają wysokie prawdopodobieństwo zachowania ważności przez wiele silników i wiele lat, a także są w miarę idiomatyczne / nieinwazyjne.
Wracając do przykładu: wewnętrzne użycie Smis powinno być szczegółem implementacyjnym, o którym kod użytkownika nie musi wiedzieć. Sprawi, że niektóre przypadki będą bardziej wydajne i nie powinny zaszkodzić w innych przypadkach. Nie wszystkie silniki używają Smis (na przykład AFAIK Firefox / Spidermonkey historycznie tego nie zrobił; słyszałem, że w niektórych przypadkach używają Smis; ale nie znam żadnych szczegółów i nie mogę rozmawiać z żadnym autorytetem w sprawie materia). W wersji 8 rozmiar Smis jest wewnętrznym szczegółem i faktycznie zmieniał się wraz z upływem czasu i wersjami. Na platformach 32-bitowych, które były najczęściej używane, Smis zawsze był 31-bitowymi liczbami całkowitymi; na platformach 64-bitowych były to 32-bitowe liczby całkowite ze znakiem, co ostatnio wydawało się najczęstszym przypadkiem, dopóki w Chrome 80 nie wprowadziliśmy „kompresji wskaźnika” dla architektur 64-bitowych, które wymagały zmniejszenia rozmiaru Smi do 31 bitów znanych z platform 32-bitowych. Jeśli zdarzyło Ci się, że oparłeś implementację na założeniu, że Smis mają zwykle 32 bity, dostaniesz niefortunne sytuacje takie jaktego .
Na szczęście, jak zauważyłeś, podwójne tablice wciąż są bardzo szybkie. W przypadku kodu obciążonego numerycznie prawdopodobnie sensowne jest przyjęcie / targetowanie podwójnych tablic. Biorąc pod uwagę częstość podwójności w JavaScript, można założyć, że wszystkie silniki mają dobre wsparcie dla podwójnych i podwójnych tablic.
Czy można wykonywać ogólne, wysokowydajne programowanie w JavaScript bez użycia czegoś takiego jak system makr, aby wstawić takie rzeczy jak vec.add () do stron wywoławczych?
„ogólny” jest generalnie sprzeczny z „wysokowydajnym”. Nie ma to związku z JavaScriptem ani z konkretnymi implementacjami silnika.
Kod „ogólny” oznacza, że decyzje muszą być podejmowane w czasie wykonywania. Za każdym razem, gdy wykonujesz funkcję, kod musi być uruchamiany, aby określić, powiedzmy: „jest x
liczbą całkowitą? Jeśli tak, weź tę ścieżkę kodu. Czy x
łańcuch? Następnie przeskocz tutaj. Czy to obiekt? Czy on ma .valueOf
? Nie? może .toString()
? Może w swoim łańcuchu prototypów? Zadzwoń i zacznij od początku z wynikiem ". Zoptymalizowany kod „wysokiej wydajności” jest zasadniczo oparty na pomiarze rezygnacji z tych wszystkich testów dynamicznych; jest to możliwe tylko wtedy, gdy silnik / kompilator ma jakiś sposób na wcześniejsze wnioskowanie o typach: jeśli może udowodnić (lub założyć z wystarczającym prawdopodobieństwem), że x
zawsze będzie liczbą całkowitą, musi wygenerować kod tylko dla tego przypadku ( pilnowany przez kontrolę typu, jeśli w grę wchodziły niepotwierdzone założenia).
Inlining jest do tego wszystkiego ortogonalny. Funkcję „ogólną” można jeszcze wprowadzić. W niektórych przypadkach kompilator może być w stanie propagować informacje o typie do funkcji wbudowanej, aby zmniejszyć tam polimorfizm.
(Dla porównania: C ++, ponieważ jest językiem kompilowanym statycznie, ma szablony do rozwiązania pokrewnego problemu. Krótko mówiąc, pozwalają programiście wyraźnie poinstruować kompilator o utworzeniu specjalistycznych kopii funkcji (lub całych klas) sparametryzowanych na danych typach. To jest fajne rozwiązanie w niektórych przypadkach, ale nie bez własnego zestawu wad, na przykład długich czasów kompilacji i dużych plików binarnych. JavaScript oczywiście nie ma czegoś takiego jak szablony. Możesz użyć eval
do zbudowania systemu, który jest nieco podobny, ale wtedy miałby podobne wady: musiałbyś wykonać ekwiwalent pracy kompilatora C ++ w czasie wykonywania i musiałbyś się martwić o ilość generowanego kodu).
Jak modularnie kodować wysokowydajny kod do bibliotek w świetle takich rzeczy, jak megamorficzne witryny wywołujące i dezoptymalizacje? Na przykład, jeśli szczęśliwie używam pakietu A Algebry Liniowej z dużą prędkością, a następnie importuję pakiet B zależny od A, ale B wywołuje go z innymi typami i dezoptymalizuje go, nagle (bez zmiany kodu) mój kod działa wolniej .
Tak, to jest ogólny problem z JavaScript. Wersja 8 służyła do wewnętrznego implementowania niektórych wbudowanych funkcji (takich jak Array.sort
) w JavaScript, a ten problem (który nazywamy „zanieczyszczeniem sprzężeniem zwrotnym typu”) był jednym z głównych powodów, dla których całkowicie odeszliśmy od tej techniki.
To powiedziawszy, dla kodu numerycznego nie ma tak wielu typów (tylko Smis i podwójne), a jak zauważyłeś, powinny one mieć podobną wydajność w praktyce, więc chociaż zanieczyszczenie sprzężeniem zwrotnym typu jest rzeczywiście teoretycznym problemem, aw niektórych przypadkach może mają znaczący wpływ, jest również całkiem prawdopodobne, że w scenariuszach algebry liniowej nie zobaczysz mierzalnej różnicy.
Ponadto w silniku występuje znacznie więcej sytuacji niż „jeden typ == szybki” i „więcej niż jeden typ == wolny”. Jeśli dana operacja widziała zarówno Smis, jak i dublet, to w porządku. Ładowanie elementów z dwóch rodzajów tablic również jest w porządku. Używamy terminu „megamorficzny” w przypadku, gdy obciążenie zobaczyło tak wiele różnych typów, że rezygnuje z indywidualnego śledzenia, a zamiast tego używa bardziej ogólnego mechanizmu, który lepiej skaluje się do dużej liczby typów - funkcja zawierająca takie obciążenia może wciąż się optymalizuje. „Dezoptymalizacja” to bardzo konkretny czyn polegający na wyrzucaniu zoptymalizowanego kodu dla funkcji, ponieważ widzi się nowy typ, którego wcześniej nie widziano, i dlatego zoptymalizowany kod nie jest przystosowany do obsługi. Ale nawet to jest w porządku: po prostu wróć do niezoptymalizowanego kodu, aby zebrać więcej informacji zwrotnych na temat typu i zoptymalizuj później. Jeśli zdarzy się to kilka razy, nie ma się czym martwić; staje się problemem tylko w patologicznie złych przypadkach.
Podsumowując, to wszystko: nie martw się o to . Wystarczy napisać rozsądny kod, niech silnik sobie z tym poradzi. Mówiąc „rozsądny”, mam na myśli: to, co ma sens w twoim przypadku użycia, jest czytelne, łatwe w utrzymaniu, wykorzystuje wydajne algorytmy, nie zawiera błędów takich jak czytanie poza długością tablic. Idealnie, to wszystko, co tam jest, i nie musisz robić nic więcej. Jeśli to sprawia, że czujesz się lepiej zrobić coś , i / lub jeśli rzeczywiście obserwując problemy z wydajnością, mogę zaoferować dwa pomysły:
Korzystanie z TypeScript może pomóc. Ostrzeżenie o dużym tłuszczu: typy TypeScript mają na celu produktywność programistów, a nie wydajność wykonywania (i jak się okazuje, te dwie perspektywy mają bardzo odmienne wymagania od systemu typów). To powiedziawszy, istnieje pewne nakładanie się: np. Jeśli konsekwentnie adnotujesz rzeczy jako number
, wtedy kompilator TS ostrzeże cię, jeśli przypadkowo umieścisz null
w tablicy lub funkcji, która powinna zawierać / operować tylko na liczbach. Oczywiście nadal wymagana jest dyscyplina: pojedynczy number_func(random_object as number)
właz ratunkowy może po cichu podważyć wszystko, ponieważ poprawność adnotacji typu nie jest nigdzie egzekwowana.
Użycie TypedArrays może również pomóc. Mają nieco więcej narzutu (zużycie pamięci i szybkość alokacji) na tablicę w porównaniu ze zwykłymi tablicami JavaScript (więc jeśli potrzebujesz wielu małych tablic, wówczas tablice zwykłe są prawdopodobnie bardziej wydajne) i są mniej elastyczne, ponieważ nie mogą się rozwijać lub zmniejszają się po alokacji, ale dają gwarancję, że wszystkie elementy mają dokładnie jeden typ.
Czy są jakieś dobre, łatwe w użyciu narzędzia pomiarowe do sprawdzania, co silnik JavaScript robi wewnętrznie z typami?
Nie, i to celowo. Jak wyjaśniono powyżej, nie chcemy, abyś specjalnie dostosowywał swój kod do wszelkich wzorców, które V8 może dziś szczególnie dobrze zoptymalizować, i nie uważamy, że tak naprawdę chcesz to zrobić. Ten zestaw rzeczy może się zmieniać w obu kierunkach: jeśli istnieje wzór, który chciałbyś zastosować, możemy go zoptymalizować w przyszłej wersji (wcześniej bawiliśmy się pomysłem przechowywania nieskartowanych 32-bitowych liczb całkowitych jako elementów tablicy .. ... ale prace nad tym jeszcze się nie rozpoczęły, więc nie ma obietnic); a czasami, jeśli w przeszłości stosowaliśmy pewien wzorzec optymalizacji, możemy zdecydować się go porzucić, jeśli przeszkadza to w innych, ważniejszych / wpływających na optymalizację. Ponadto rzeczy takie jak inline heurystyka są notorycznie trudne do poprawienia, więc podejmowanie właściwych decyzji we właściwym czasie jest obszarem ciągłych badań i odpowiednich zmian w zachowaniu silnika / kompilatora; co sprawia, że jest to kolejny przypadek, w którym byłoby to niefortunne dla wszystkich (ciebiei nas), jeśli spędziłeś dużo czasu na ulepszaniu kodu, dopóki jakiś zestaw bieżących wersji przeglądarek nie podejmie w przybliżeniu decyzji, które według ciebie (lub wiesz?) są najlepsze, tylko po to, aby wrócić pół roku później, aby zdać sobie sprawę, że obecne przeglądarki zmieniły swoją heurystykę.
Oczywiście zawsze możesz zmierzyć wydajność całej aplikacji - to jest to, co ostatecznie się liczy, a nie to, jakie wybory konkretnie dokonał silnik. Uważaj na mikrobenchmarki, ponieważ są one mylące: jeśli wyodrębnisz tylko dwa wiersze kodu i porównasz je, istnieje szansa, że scenariusz będzie wystarczająco inny (np. Informacje zwrotne innego typu), że silnik podejmie bardzo różne decyzje.