Bagaimana fungsi util.toFastProperties dari Bluebird membuat properti suatu objek “cepat”?

165

Dalam util.jsfile Bluebird , ia memiliki fungsi sebagai berikut:

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

Untuk beberapa alasan, ada pernyataan setelah fungsi pengembalian, yang saya tidak yakin mengapa itu ada di sana.

Selain itu, tampaknya disengaja, karena penulis telah membungkam peringatan JSHint tentang ini:

'Eval' yang tidak terjangkau setelah 'kembali'. (W027)

Apa sebenarnya fungsi ini lakukan? Apakah util.toFastPropertiesbenar-benar membuat properti objek lebih cepat?

Saya telah mencari di repositori GitHub Bluebird untuk mendapatkan komentar dalam kode sumber atau penjelasan dalam daftar masalah mereka, tetapi saya tidak dapat menemukannya.

Qantas 94 Heavy
sumber

Jawaban:

314

Pembaruan 2017: Pertama, untuk pembaca yang datang hari ini - ini adalah versi yang berfungsi dengan 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
}

Sans satu atau dua optimasi kecil - semua di bawah ini masih berlaku.

Pertama mari kita bahas apa yang dilakukannya dan mengapa itu lebih cepat dan kemudian mengapa itu berhasil.

Apa yang dilakukannya

Mesin V8 menggunakan dua representasi objek:

  • Mode kamus - di mana objek disimpan sebagai kunci - peta nilai sebagai peta hash .
  • Mode cepat - di mana objek disimpan seperti struct , di mana tidak ada perhitungan yang terlibat dalam akses properti.

Berikut ini adalah demo sederhana yang menunjukkan perbedaan kecepatan. Di sini kita menggunakan deletepernyataan untuk memaksa objek ke mode kamus lambat.

Mesin mencoba menggunakan mode cepat bila memungkinkan dan umumnya setiap kali banyak akses properti dilakukan - namun terkadang ia dilemparkan ke mode kamus. Berada dalam mode kamus memiliki penalti performa yang besar sehingga umumnya diinginkan untuk meletakkan objek dalam mode cepat.

Retasan ini dimaksudkan untuk memaksa objek ke mode cepat dari mode kamus.

Kenapa lebih cepat

Dalam JavaScript prototipe biasanya menyimpan fungsi yang dibagikan di antara banyak contoh dan jarang berubah banyak secara dinamis. Untuk alasan ini sangat diinginkan untuk memilikinya dalam mode cepat untuk menghindari penalti tambahan setiap kali suatu fungsi dipanggil.

Untuk ini - v8 akan dengan senang hati meletakkan objek yang merupakan .prototype properti fungsi dalam mode cepat karena mereka akan dibagikan oleh setiap objek yang dibuat dengan menggunakan fungsi tersebut sebagai konstruktor. Ini pada umumnya optimasi yang cerdas dan diinginkan.

Bagaimana itu bekerja

Mari kita pertama-tama membaca kode dan mencari tahu apa yang dilakukan setiap baris:

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.
}

Kita tidak harus menemukan kode sendiri untuk menyatakan bahwa v8 melakukan optimasi ini, kita bisa membaca tes unit v8 :

// 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));

Membaca dan menjalankan tes ini menunjukkan kepada kita bahwa optimasi ini memang berfungsi di v8. Namun - akan menyenangkan untuk melihat caranya.

Jika kita periksa, objects.cckita dapat menemukan fungsi berikut (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);
  }
}

Sekarang, JSObject::MigrateSlowToFastsecara eksplisit mengambil Kamus dan mengubahnya menjadi objek V8 cepat. Ini adalah bacaan yang bermanfaat dan wawasan yang menarik tentang objek internal v8 - tapi itu bukan subjek di sini. Saya masih dengan hangat menyarankan agar Anda membacanya di sini karena ini adalah cara yang baik untuk belajar tentang objek v8.

Jika kita check- SetPrototypein objects.cc, kita dapat melihat bahwa itu disebut dalam baris 12231:

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

Yang pada gilirannya disebut dengan FuntionSetPrototypeapa yang kita dapatkan .prototype =.

Melakukan __proto__ =atau .setPrototypeOfmungkin juga bekerja tetapi ini adalah fungsi ES6 dan Bluebird berjalan di semua browser sejak Netscape 7 sehingga tidak ada pertanyaan untuk menyederhanakan kode di sini. Misalnya, jika kita periksa, .setPrototypeOfkita dapat melihat:

// 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;
}

Yang langsung aktif Object:

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

Jadi - kita telah menapaki jalan dari kode yang ditulis Petka ke bare metal. Ini bagus.

Penolakan:

Ingat ini semua detail implementasi. Orang-orang seperti Petka adalah penggila optimasi. Selalu ingat bahwa pengoptimalan prematur adalah akar dari semua kejahatan 97% dari waktu. Bluebird sering melakukan sesuatu yang sangat mendasar sehingga mendapatkan banyak keuntungan dari peretasan kinerja ini - menjadi secepat panggilan balik tidak mudah. Anda jarang harus melakukan sesuatu seperti ini dalam kode yang tidak mendukung perpustakaan.

Benjamin Gruenbaum
sumber
37
Ini adalah posting paling menarik yang pernah saya baca. Banyak rasa hormat dan penghargaan untuk Anda!
m59
2
@timoxley saya menulis yang berikut tentang eval(dalam komentar kode ketika menjelaskan kode OP diposting): "mencegah fungsi dari dioptimalkan melalui penghapusan kode mati atau optimasi lebih lanjut. Kode ini tidak pernah tercapai tetapi bahkan kode yang tidak terjangkau menyebabkan v8 untuk tidak mengoptimalkan fungsi. " . Inilah bacaan terkait . Apakah Anda ingin saya menjelaskan lebih lanjut tentang masalah ini?
Benjamin Gruenbaum
3
@dherman a 1;tidak akan menyebabkan "deoptimisasi", debugger;mungkin akan bekerja dengan baik. Yang menyenangkan adalah bahwa ketika evaldilewati sesuatu yang bukan string, ia tidak melakukan apa-apa dengan itu sehingga agak tidak berbahaya - sepertiif(false){ debugger; }
Benjamin Gruenbaum
6
Btw kode ini telah diperbarui karena perubahan pada v8 baru-baru ini, sekarang Anda perlu instantiate juga konstruktor. Jadi itu menjadi lebih malas; d
Esailija
4
@BenjaminGruenbaum Bisakah Anda menjelaskan mengapa fungsi ini TIDAK dioptimalkan? Dalam kode yang diperkecil, eval tidak ada. Mengapa eval berguna di sini dalam kode non-minified?
Boopathi Rajaa