Frozen Prototypes
With the addition of Object.freeze
and Object.seal
in ECMAScript 5.1, there's a way for developers to prevent various kinds of mutations to objects. For example, Object.freeze
can be used to make an object essentially immutable.
"use strict";
const obj = { a: "Hello", b: "World" };
Object.freeze(obj);
obj.a = "Hallo"; // Throws a TypeError
obj.c = "!"; // Also throws a TypeError
So from a developer's point of view this looks like a very useful way to guard against changes. And looking at it naively, it also seems to offer an opportunity for the JavaScript engines to optimize, because the objects cannot change anymore. However that's not the case in V8, at least currently, for several reasons. A lot of this is because various parts of the engine aren't optimized for frozen / sealed objects at this point.
One particularly bad example was reported yesterday: Freezing the Array.prototype
or the Object.prototype
, as for example done by Apache weex, causes several of the Array
builtins inside V8 to miss on the fast-path and take the generic slow-path instead, which can be an order of magnitude slower. This affects Array.prototype.slice
, Array.prototype.splice
, and several other builtins. Consider the following simple micro-benchmark:
function testSplice(a) {
for (var i = 0; i < 1e6; ++i) a = a.splice();
return a;
}
function testSlice(a) {
for (var i = 0; i < 1e6; ++i) a = a.slice();
return a;
}
const TESTS = [testSplice, testSlice];
const n = 1e6;
let a = new Array(n);
for (var i = 0; i < n; ++i) a[i] = i;
for (const test of TESTS) {
console.time(test.name);
a = test(a);
console.timeEnd(test.name);
}
Object.freeze(Array.prototype);
for (const test of TESTS) {
const name = test.name + " (frozen)";
console.time(name);
a = test(a);
console.timeEnd(name);
}
Running this on Chrome 60 (current stable channel) shows an approximately 5x slow-down on Array.prototype.splice
and roughly 4x slow-down on Array.prototype.slice
in this particular micro-benchmark. This particular issue is now fixed for the upcoming Chrome 62.
The reason why Object.freeze
on the Array.prototype
(or the Object.prototype
) redirects several Array
builtins to their slow-paths is that the fast-paths cannot deal with array elements (properties whose names are unsigned integers in the range 0 to 4294967295) in the prototype chain, and specifically accessors on elements cannot be handled in the fast-paths. So these builtins do a quickcheck in the beginning to see if all prototypes are regular objects (i.e. there are no proxies in the prototype chain), and all of the prototypes have no elements.
By default neither the Object.prototype
nor the Array.prototype
have elements, and Object.freeze
doesn't add any elements. However, Object.freeze
currently has to put the objects into DICTIONARY_ELEMENTS
mode at this point (for implementation reasons), which means that the object will have a different marker when it doesn't have any elements (the empty_slow_elements_dictionary
instead of the empty_fixed_array
that is used for fast elements objects). Unfortunately the helper functions used by the affected Array
builtins only checked the prototypes for empty_fixed_array
, but not for empty_slow_elements_dictionary
, so they would automatically fall back to the generic, safe route instead of attempting the fast-path.
diff --git a/src/objects-inl.h b/src/objects-inl.h
index 010ac2e06e..68f24baf15 100644
--- a/src/objects-inl.h
+++ b/src/objects-inl.h
@@ -902,11 +902,17 @@ bool JSObject::PrototypeHasNoElements(Isolate* isolate, JSObject* object) {
DisallowHeapAllocation no_gc;
HeapObject* prototype = HeapObject::cast(object->map()->prototype());
HeapObject* null = isolate->heap()->null_value();
- HeapObject* empty = isolate->heap()->empty_fixed_array();
+ HeapObject* empty_fixed_array = isolate->heap()->empty_fixed_array();
+ HeapObject* empty_slow_element_dictionary =
+ isolate->heap()->empty_slow_element_dictionary();
while (prototype != null) {
Map* map = prototype->map();
if (map->instance_type() <= LAST_CUSTOM_ELEMENTS_RECEIVER) return false;
- if (JSObject::cast(prototype)->elements() != empty) return false;
+ HeapObject* elements = JSObject::cast(prototype)->elements();
+ if (elements != empty_fixed_array &&
+ elements != empty_slow_element_dictionary) {
+ return false;
+ }
prototype = HeapObject::cast(map->prototype());
}
return true;
As such, the relevant changes to JSObject::PrototypeHasNoElements
in the V8 runtime (and similarly in the CodeStubAssembler
) were fairly straight-forward once the problem was identified.