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 delete
instrukcji, 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ą .prototype
wł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::MigrateSlowToFast
po 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ć SetPrototype
w 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 FuntionSetPrototype
to, co otrzymujemy .prototype =
.
Robi __proto__ =
lub .setPrototypeOf
bę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.