How does Bluebird's util.toFastProperties function make an object's properties "fast"?
Asked Answered
T

4

169

In Bluebird's util.js file, it has the following function:

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

For some reason, there's a statement after the return function, which I'm not sure why it's there.

As well, it seems that it is deliberate, as the author had silenced the JSHint warning about this:

Unreachable 'eval' after 'return'. (W027)

What exactly does this function do? Does util.toFastProperties really make an object's properties "faster"?

I've searched through Bluebird's GitHub repository for any comments in the source code or an explanation in their list of issues, but I couldn't find any.

Talus answered 28/7, 2014 at 3:0 Comment(0)
P
325

2017 update: First, for readers coming today - here is a version that works with 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 one or two small optimizations - all the below is still valid.


Let's first discuss what it does and why that's faster and then why it works.

What it does

The V8 engine uses two object representations:

  • Dictionary mode - in which object are stored as key - value maps as a hash map.
  • Fast mode - in which objects are stored like structs, in which there is no computation involved in property access.

Here is a simple demo that demonstrates the speed difference. Here we use the delete statement to force the objects into slow dictionary mode.

The engine tries to use fast mode whenever possible and generally whenever a lot of property access is performed - however sometimes it gets thrown into dictionary mode. Being in dictionary mode has a big performance penalty so generally it is desirable to put objects in fast mode.

This hack is intended to force the object into fast mode from dictionary mode.

Why it's faster

In JavaScript prototypes typically store functions shared among many instances and rarely change a lot dynamically. For this reason it is very desirable to have them in fast mode to avoid the extra penalty every time a function is called.

For this - v8 will gladly put objects that are the .prototype property of functions in fast mode since they will be shared by every object created by invoking that function as a constructor. This is generally a clever and desirable optimization.

How it works

Let's first go through the code and figure what each line does:

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

We don't have to find the code ourselves to assert that v8 does this optimization, we can instead read the v8 unit tests:

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

Reading and running this test shows us that this optimization indeed works in v8. However - it would be nice to see how.

If we check objects.cc we can find the following function (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);
    }
}

Now, JSObject::MigrateSlowToFast just explicitly takes the Dictionary and converts it into a fast V8 object. It's a worthwhile read and an interesting insight into v8 object internals - but it's not the subject here. I still warmly recommend that you read it here as it's a good way to learn about v8 objects.

If we check out SetPrototype in objects.cc, we can see that it is called in line 12231:

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

Which in turn is called by FuntionSetPrototype which is what we get with .prototype =.

Doing __proto__ = or .setPrototypeOf would have also worked but these are ES6 functions and Bluebird runs on all browsers since Netscape 7 so that's out of the question to simplify code here. For example, if we check .setPrototypeOf we can see:

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

Which directly is on Object:

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

So - we have walked the path from the code Petka wrote to the bare metal. This was nice.

Disclaimer:

Remember this is all implementation detail. People like Petka are optimization freaks. Always remember that premature optimization is the root of all evil 97% of the time. Bluebird does something very basic very often so it gains a lot from these performance hacks - being as fast as callbacks isn't easy. You rarely have to do something like this in code that doesn't power a library.

Pleochroism answered 28/7, 2014 at 6:57 Comment(9)
This is the most interesting post I've read in a while. Much respect and appreciation to you!Radbourne
+1 because of, well, it's interesting and much detailed. But this doesn't look like premature optimization to me. It might maybe look excessive but in my book a premature optimization is one that has side effects on the structuring of you data or program and that is hard to remove and make refactoring more complex. But I agree developers not deeply following the field should avoid this kind of optimization which is usually better done by optimizers like the one of V8, it's painful to read codes with clever tricks only making the code slower..Bodnar
does this explain the eval after the return somehow that I missed?Nolin
@Nolin I wrote the following about the eval (in the code comments when explaining the code OP posted): "prevent the function from being optimized through dead code elimination or further optimizations. This code is never reached but even unreachable code causes v8 to not optimize functions." . Here's a related read. Would you like me to elaborate further on the subject?Pleochroism
@BenjaminGruenbaum Is there something special about eval that makes it useful in that context? If it's just dead code, wouldn't a simple statement like 1; be sufficient?Bedridden
@Bedridden a 1; would not cause a "deoptimization", a debugger; would have probably worked equally well. The nice thing is that when eval is passed something that is not a string it doesn't do anything with it so it's rather harmless - kind of like if(false){ debugger; }Pleochroism
Btw this code has been updated due to a change in recent v8, now you need to instantiate the constructor too. So it became lazier ;dOleson
@BenjaminGruenbaum Can you elaborate on why this function should be NOT optimized ? In the minified code, eval is anyway not present. Why is eval useful here in the non-minified code?Smew
@BoopathiRajaa indeed, in the minified version the eval is removed - but honestly the performance impact on the client side is tiny. This is indeed a problem, just not a big one.Pleochroism
V
4

V8 developer here. The accepted answer is a great explanation, I just wanted to highlight one thing: the so-called "fast" and "slow" property modes are unfortunate misnomers, they each have their pros and cons. Here is a (slightly simplified) overview of the performance of various operations:

struct-like properties dictionary properties
adding a property to an object -- +
deleting a property --- +
reading/writing a property, first time - +
reading/writing, cached, monomorphic +++ +
reading/writing, cached, few shapes ++ +
reading/writing, cached, many shapes -- +
colloquial name "fast" "slow"

So as you can see, dictionary properties are actually faster for most of the lines in this table, because they don't care what you do, they just handle everything with solid (though not record-breaking) performance. Struct-like properties are blazing fast for one particular situation (reading/writing the values of existing properties, where every individual place in the code only sees very few distinct object shapes), but the price they pay for that is that all other operations, in particular those that add or remove properties, become much slower.

It just so happens that the special case where struct-like properties have their big advantage (+++) is particularly frequent and really important for many apps' performance, which is why they acquired the "fast" moniker. But it's important to realize that when you delete properties and V8 switches the affected objects to dictionary mode, then it isn't being dumb or trying to be annoying: rather it attempts to give you the best possible performance for what you're doing. We have landed patches in the past that have achieved significant performance improvements by making more objects go to dictionary ("slow") mode sooner when appropriate.

Now, it can happen that your objects would generally benefit from struct-like properties, but something your code does causes V8 to transition them to dictionary properties, and you'd like to undo that; Bluebird had such a case. Still, the name toFastProperties is a bit misleading in its simplicity; a more accurate (though unwieldy) name would be spendTimeOptimizingThisObjectAssumingItsPropertiesWontChange, which would indicate that the operation itself is costly, and it only makes sense in certain limited cases. If someone took away the conclusion "oh, this is great, so I can happily delete properties now, and just call toFastProperties afterwards every time", then that would be a major misunderstanding and cause pretty bad performance degradation.

If you stick with a few simple rules of thumb, you'll never have a reason to even try to force any internal object representation changes:

  • Use constructors, and initialize all properties in the constructor. (This helps not only your engine, but also understandability and maintainability of your code. Consider that TypeScript doesn't quite force this but strongly encourages it, because it helps engineering productivity.)
  • Use classes or prototypes to install methods, don't just slap them onto each object instance. (Again, this is a common best practice for many reasons, one of them being that it's faster.)
  • Avoid delete. When properties come and go, prefer using a Map over the ES5-era "object-as-map" pattern. When an object can toggle into and out of a certain state, prefer boolean (or equivalent) properties (e.g. o.has_state = true; o.has_state = false;) over adding and deleting an indicator property.
  • When it comes to performance, measure, measure, measure. Before you start sinking time into performance improvements, profile your app to see where the hotspots are. When you implement a change that you hope will make things faster, verify with your real app (or something extremely close to it; not just a 10-line microbenchmark!) that it actually helps.

Lastly, if your team lead tells you "I've heard that there are 'fast' and 'slow' properties, please make sure that all of ours are 'fast'", then point them at this post :-)

Virgil answered 12/6, 2022 at 13:41 Comment(2)
"prefer boolean properties over adding and deleting an indicator property." - or set the indicator property to null/undefined for the states where it shouldn't have any normal valueFantan
@Bergi: yup, that's exactly what I meant by "boolean (or equivalent)" :-)Virgil
P
1

Reality from 2021 (NodeJS version 12+). Seems like a huge optimization is done, objects with deleted fields and sparse arrays don't become slow. Or I'm missing smth?

// run in Node with enabled flag
// node --allow-natives-syntax script.js

function Point(x, y) {
  this.x = x;
  this.y = y;
}

var obj1 = new Point(1, 2);
var obj2 = new Point(3, 4);
delete obj2.y;

var arr = [1,2,3]
arr[100] = 100

console.log('obj1 has fast properties:', %HasFastProperties(obj1));
console.log('obj2 has fast properties:', %HasFastProperties(obj2));
console.log('arr has fast properties:', %HasFastProperties(arr));

both show true

obj1 has fast properties: true
obj2 has fast properties: true
arr has fast properties: true
Polymerization answered 21/1, 2021 at 14:31 Comment(1)
Array elements and properties have always been distinct under the hood. An array can have struct-like properties and dictionary-mode elements, or vice versa. -- For a couple of years there's been a special case where deleting the last property simply rolls back its addition, without transitioning the object to dictionary mode. Try delete obj1.x and it'll go to dictionary mode. (Which really is "fast for adding/deleting properties"-mode.)Virgil
F
-1
// run in Node with enabled flag
// node --allow-natives-syntax script.js

function Point(x, y) {
  this.x = x;
  this.y = y;
}

var obj2 = new Point(3, 4);
console.log('obj has fast properties:', %HasFastProperties(obj2)) // true
delete obj2.y;
console.log('obj2 has fast properties:', %HasFastProperties(obj2)); //true

var obj = {x : 1, y : 2};
console.log('obj has fast properties:', %HasFastProperties(obj))  //true
delete obj.x;

console.log('obj has fast properties:', %HasFastProperties(obj)); //fasle

Function and object look different

Fajardo answered 29/10, 2021 at 6:44 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Carriole

© 2022 - 2024 — McMap. All rights reserved.