W jaki sposób funkcja util.toFastProperties w Bluebird sprawia, że ​​właściwości obiektu są „szybkie”?

165

W util.jspliku Bluebird ma następującą funkcję:

function toFastProperties(obj) {
    /*jshint -W027*/
    function f() {}
    f.prototype = obj;
    ASSERT("%HasFastProperties", true, obj);
    return f;
    eval(obj);
}

Z jakiegoś powodu po funkcji return znajduje się instrukcja, której nie jestem pewien, dlaczego się tam znajduje.

Wydaje się również, że jest to celowe, ponieważ autor uciszył ostrzeżenie JSHint dotyczące tego:

Nieosiągalny „eval” po „return”. (W027)

Co dokładnie robi ta funkcja? Czy util.toFastPropertiesnaprawdę sprawia, że ​​właściwości obiektu są „szybsze”?

Przeszukałem repozytorium GitHub firmy Bluebird pod kątem jakichkolwiek komentarzy w kodzie źródłowym lub wyjaśnień na liście problemów, ale nie znalazłem żadnych.

Qantas 94 Heavy
źródło

Odpowiedzi:

314

Aktualizacja 2017: Po pierwsze, dla przyszłych czytelników - oto wersja współpracująca z Node 7 (4+):

function enforceFastProperties(o) {
    function Sub() {}
    Sub.prototype = o;
    var receiver = new Sub(); // create an instance
    function ic() { return typeof receiver.foo; } // perform access
    ic(); 
    ic();
    return o;
    eval("o" + o); // ensure no dead code elimination
}

Bez jednej lub dwóch małych optymalizacji - wszystkie poniższe są nadal aktualne.

Najpierw omówmy, co to robi i dlaczego jest szybsze, a następnie dlaczego działa.

Co to robi

Silnik V8 wykorzystuje dwie reprezentacje obiektów:

  • Tryb słownika - w którym obiekty są przechowywane jako mapy klucz-wartość jako mapa skrótów .
  • Tryb szybki - w którym obiekty są przechowywane jak struktury , w których nie ma obliczeń związanych z dostępem do właściwości.

Oto proste demo, które pokazuje różnicę prędkości. Tutaj używamy deleteinstrukcji, aby wymusić na obiektach tryb powolnego słownika.

Silnik stara się używać trybu szybkiego, gdy tylko jest to możliwe i ogólnie, gdy wykonywany jest duży dostęp do właściwości - jednak czasami zostaje wrzucony do trybu słownikowego. Praca w trybie słownika ma duży wpływ na wydajność, więc ogólnie pożądane jest umieszczanie obiektów w trybie szybkim.

Ten hack ma na celu wymuszenie przejścia obiektu do trybu szybkiego z trybu słownika.

Dlaczego to jest szybsze

W prototypach JavaScript zazwyczaj przechowują funkcje wspólne dla wielu instancji i rzadko zmieniają się bardzo dynamicznie. Z tego powodu bardzo pożądane jest, aby były w trybie szybkim, aby uniknąć dodatkowej kary za każdym razem, gdy wywoływana jest funkcja.

W tym celu - v8 chętnie umieści obiekty, które są .prototypewłasnością funkcji, w trybie szybkim, ponieważ będą one współdzielone przez każdy obiekt utworzony przez wywołanie tej funkcji jako konstruktora. Na ogół jest to sprytna i pożądana optymalizacja.

Jak to działa

Najpierw przejrzyjmy kod i zobaczmy, co robi każda linia:

function toFastProperties(obj) {
    /*jshint -W027*/ // suppress the "unreachable code" error
    function f() {} // declare a new function
    f.prototype = obj; // assign obj as its prototype to trigger the optimization
    // assert the optimization passes to prevent the code from breaking in the
    // future in case this optimization breaks:
    ASSERT("%HasFastProperties", true, obj); // requires the "native syntax" flag
    return f; // return it
    eval(obj); // prevent the function from being optimized through dead code 
               // elimination or further optimizations. This code is never  
               // reached but even using eval in unreachable code causes v8
               // to not optimize functions.
}

Nie musimy sami znajdować kodu, aby stwierdzić, że wersja 8 przeprowadza tę optymalizację, zamiast tego możemy przeczytać testy jednostkowe w wersji 8 :

// Adding this many properties makes it slow.
assertFalse(%HasFastProperties(proto));
DoProtoMagic(proto, set__proto__);
// Making it a prototype makes it fast again.
assertTrue(%HasFastProperties(proto));

Przeczytanie i uruchomienie tego testu pokazuje nam, że ta optymalizacja rzeczywiście działa w wersji 8. Jednak - fajnie by było zobaczyć jak.

Jeśli sprawdzimy objects.cc, możemy znaleźć następującą funkcję (L9925):

void JSObject::OptimizeAsPrototype(Handle<JSObject> object) {
  if (object->IsGlobalObject()) return;

  // Make sure prototypes are fast objects and their maps have the bit set
  // so they remain fast.
  if (!object->HasFastProperties()) {
    MigrateSlowToFast(object, 0);
  }
}

Teraz JSObject::MigrateSlowToFastpo prostu jawnie pobiera Dictionary i konwertuje go na szybki obiekt V8. Warto przeczytać i interesujący wgląd w wewnętrzne elementy obiektów w wersji 8 - ale nie jest to temat tutaj. Wciąż gorąco zachęcam do przeczytania tego tutaj, ponieważ jest to dobry sposób na poznanie obiektów w wersji 8.

Jeśli mamy sprawdzić SetPrototypew objects.cc, widzimy, że to się nazywa w linii 12231:

if (value->IsJSObject()) {
    JSObject::OptimizeAsPrototype(Handle<JSObject>::cast(value));
}

Który z kolei jest nazywany przez FuntionSetPrototypeto, co otrzymujemy .prototype =.

Robi __proto__ =lub .setPrototypeOfbędzie również pracował, ale są to funkcje ES6 i Bluebird działa na wszystkich przeglądarkach Netscape 7 od czasu więc to nie wchodzi w rachubę do kodu uprościć tutaj. Na przykład, jeśli sprawdzimy .setPrototypeOf, zobaczymy:

// ES6 section 19.1.2.19.
function ObjectSetPrototypeOf(obj, proto) {
  CHECK_OBJECT_COERCIBLE(obj, "Object.setPrototypeOf");

  if (proto !== null && !IS_SPEC_OBJECT(proto)) {
    throw MakeTypeError("proto_object_or_null", [proto]);
  }

  if (IS_SPEC_OBJECT(obj)) {
    %SetPrototype(obj, proto); // MAKE IT FAST
  }

  return obj;
}

Który bezpośrednio jest włączony Object:

InstallFunctions($Object, DONT_ENUM, $Array(
...
"setPrototypeOf", ObjectSetPrototypeOf,
...
));

A więc - przeszliśmy ścieżkę od kodu napisanego przez Petkę do gołego metalu. To było miłe.

Zrzeczenie się:

Pamiętaj, że to wszystkie szczegóły implementacji. Ludzie tacy jak Petka są maniakami optymalizacji. Zawsze pamiętaj, że przedwczesna optymalizacja jest źródłem wszelkiego zła w 97% przypadków. Bluebird bardzo często robi coś bardzo podstawowego, więc wiele zyskuje na tych hackach wydajnościowych - bycie tak szybkim jak callback nie jest łatwe. Ty rzadko zrobić coś takiego w kodzie, który nie zasila biblioteka.

Benjamin Gruenbaum
źródło
37
To najciekawszy post, jaki od jakiegoś czasu czytałem. Ogromny szacunek i uznanie dla Ciebie!
m59
2
@timoxley Napisałem o tym eval(w komentarzach do kodu, wyjaśniając opublikowany kod OP): "zapobiegaj optymalizacji funkcji poprzez eliminację martwego kodu lub dalsze optymalizacje. Ten kod nigdy nie zostanie osiągnięty, ale nawet kod nieosiągalny powoduje, że wersja 8 nie jest optymalizowana Funkcje." . Oto pokrewna lektura . Czy chciałbyś, żebym rozwinął temat dalej?
Benjamin Gruenbaum
3
@dherman a 1;nie spowodowałby „dezoptymalizacji”, debugger;prawdopodobnie działałby równie dobrze. Dobrą rzeczą jest to, że gdy evaljest przekazywana coś, co nie jest ciągiem to nie ma nic z tym zrobić, więc jest to raczej nieszkodliwy - tak jakbyif(false){ debugger; }
Benjamin Gruenbaum
6
Przy okazji ten kod został zaktualizowany z powodu zmiany w ostatniej wersji 8, teraz musisz również utworzyć wystąpienie konstruktora. Więc stało się bardziej leniwe; d
Esailija
4
@BenjaminGruenbaum Czy możesz wyjaśnić, dlaczego NIE należy optymalizować tej funkcji? W zminimalizowanym kodzie eval i tak nie jest obecny. Dlaczego eval jest przydatny w niezminifikowanym kodzie?
Boopathi Rajaa