Cập nhật 2017: Đầu tiên, dành cho độc giả đến hôm nay - đây là phiên bản hoạt động với 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 một hoặc hai tối ưu hóa nhỏ - tất cả những điều dưới đây vẫn còn hiệu lực.
Trước tiên chúng ta hãy thảo luận về những gì nó làm và tại sao nó nhanh hơn và sau đó tại sao nó hoạt động.
Những gì nó làm
Động cơ V8 sử dụng hai biểu diễn đối tượng:
- Chế độ từ điển - trong đó đối tượng được lưu trữ dưới dạng bản đồ khóa - giá trị dưới dạng bản đồ băm .
- Chế độ nhanh - trong đó các đối tượng được lưu trữ như các cấu trúc , trong đó không có tính toán liên quan đến truy cập thuộc tính.
Dưới đây là một bản demo đơn giản thể hiện sự khác biệt về tốc độ. Ở đây chúng tôi sử dụng delete
câu lệnh để buộc các đối tượng vào chế độ từ điển chậm.
Công cụ cố gắng sử dụng chế độ nhanh bất cứ khi nào có thể và nói chung bất cứ khi nào có nhiều quyền truy cập thuộc tính được thực hiện - tuy nhiên đôi khi nó bị ném vào chế độ từ điển. Ở trong chế độ từ điển có một hình phạt hiệu suất lớn vì vậy nhìn chung nên đặt các đối tượng ở chế độ nhanh.
Bản hack này nhằm buộc đối tượng vào chế độ nhanh từ chế độ từ điển.
Tại sao nó nhanh hơn
Trong các nguyên mẫu JavaScript thường lưu trữ các hàm được chia sẻ giữa nhiều trường hợp và hiếm khi thay đổi nhiều. Vì lý do này, rất mong muốn có chúng ở chế độ nhanh để tránh bị phạt thêm mỗi khi một chức năng được gọi.
Đối với điều này - v8 sẽ sẵn sàng đặt các đối tượng là thuộc .prototype
tính của các hàm ở chế độ nhanh vì chúng sẽ được chia sẻ bởi mọi đối tượng được tạo bằng cách gọi hàm đó là hàm tạo. Đây thường là một tối ưu hóa thông minh và mong muốn.
Làm thế nào nó hoạt động
Trước tiên chúng ta hãy đi qua mã và tìm hiểu mỗi dòng làm gì:
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.
}
Chúng tôi không phải tự tìm mã để khẳng định rằng v8 thực hiện tối ưu hóa này, thay vào đó chúng tôi có thể đọc các bài kiểm tra đơn vị 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));
Đọc và chạy thử nghiệm này cho chúng ta thấy rằng tối ưu hóa này thực sự hoạt động trong v8. Tuy nhiên - thật tuyệt khi thấy cách làm.
Nếu chúng ta kiểm tra, objects.cc
chúng ta có thể tìm thấy chức năng sau (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);
}
}
Bây giờ, JSObject::MigrateSlowToFast
chỉ cần lấy từ điển một cách rõ ràng và chuyển đổi nó thành một đối tượng V8 nhanh. Đây là một bài đáng đọc và một cái nhìn sâu sắc thú vị về nội bộ đối tượng v8 - nhưng nó không phải là chủ đề ở đây. Tôi vẫn nhiệt liệt khuyên bạn nên đọc nó ở đây vì đây là một cách tốt để tìm hiểu về các đối tượng v8.
Nếu chúng tôi kiểm tra SetPrototype
trong objects.cc
, chúng ta có thể thấy rằng nó được gọi là phù hợp 12.231:
if (value->IsJSObject()) {
JSObject::OptimizeAsPrototype(Handle<JSObject>::cast(value));
}
Mà lần lượt được gọi bởi FuntionSetPrototype
đó là những gì chúng ta nhận được với .prototype =
.
Làm __proto__ =
hoặc .setPrototypeOf
sẽ làm việc nhưng đây là các chức năng ES6 và Bluebird chạy trên tất cả các trình duyệt kể từ Netscape 7, vì vậy đó không phải là câu hỏi để đơn giản hóa mã ở đây. Ví dụ: nếu chúng tôi kiểm tra, .setPrototypeOf
chúng tôi có thể thấy:
// 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;
}
Mà trực tiếp là trên Object
:
InstallFunctions($Object, DONT_ENUM, $Array(
...
"setPrototypeOf", ObjectSetPrototypeOf,
...
));
Vì vậy - chúng tôi đã đi theo con đường từ mã Petka đã viết đến kim loại trần. Điều này thật tuyệt
Tuyên bố từ chối trách nhiệm:
Hãy nhớ rằng đây là tất cả các chi tiết thực hiện. Những người như Petka là tối ưu hóa quái vật. Luôn nhớ rằng tối ưu hóa sớm là gốc rễ của mọi tội lỗi 97% thời gian. Bluebird thực hiện một cái gì đó rất cơ bản rất thường xuyên để nó thu được rất nhiều từ các bản hack hiệu suất này - nhanh như gọi lại không dễ dàng. Bạn hiếm khi phải làm một cái gì đó như thế này trong mã không cung cấp năng lượng cho thư viện.