Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Frozen Prototypes

DZone's Guide to

Frozen Prototypes

Freezing and sealing can be used to make an object immutable in JavaScript, but freezing causes major performance problems in V8.

· Performance Zone
Free Resource

Transform incident management with machine learning and analytics to help you maintain optimal performance and availability while keeping pace with the growing demands of digital business with this eBook, brought to you in partnership with BMC.

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.

Slow-down of Array#slice and Array#splice

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 quick check 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 default neither the Object.prototype nor the the Array.prototype have elements, and elements, and Object.freeze doesn’t add any elements. However, Object.freeze currently has to put the objects into 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 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 the empty_fixed_array that is used for fast elements objects). Unfortunately, the helper functions used by the affected Unfortunately the helper functions used by the affected Array builtins only checked the prototypes for empty_fixed_array, but not for 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.

Evolve your approach to Application Performance Monitoring by adopting five best practices that are outlined and explored in this e-book, brought to you in partnership with BMC.

Topics:
performance ,javascript ,freeze ,web performance

Published at DZone with permission of Benedikt Meurer, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}