Daniel Clifford on Optimizing JavaScript for the V8 Engine

For those of you like myself that didn’t have the good fortune of going to Google I/O, I hope you caught this video, “Breaking the JavaScript Speed Barrier:”

As an aspiring developer, this was far and away the most intriguing and helpful video from the conference. This talk, delivered by Google’s Daniel Clifford, provides a number of essential guidelines for writing JavaScript that is better optimized for running on Google Chrome’s V8 JavaScript Engine. Google has been doing pretty incredible things in the last few years with JavaScript, improving benchmarks and narrowing the speed gap between JavaScript and other languages that was once thought to be unbridgeable.

We should be grateful that Google has invested so much time and energy into optimizing JavaScript performance. It has never been more important as a language, and its star is unlikely to fade anytime soon. For Clifford, optimizing JavaScript performance not only helps us do the same old things faster and better. It also broadens our development horizons and transforms the kinds of things that are possible, especially in the sphere of front-end development.

I won’t give a fully fleshed-out summary of the talk, as I would recommend watching it on your own. It’s briskly presented and quite conversational. But I will lay out some of the bits that struck me as the most essential take-home points.

1. Be aware of hidden classes. Hidden classes constitute one of the crucial architectural elements of the V8. JavaScript has no explicit type system (as in Java and other languages). While this is often pragmatically useful, according to Clifford it can be costly at compile time because any JavaScript interpreter has to figure out what data type is being used.

Constructing a class is somewhat expensive in terms of time to begin with, but it gets much cheaper after that. Unless you going around adding properties to the hidden class, which will slow things down a great deal. Here’s how not to do things (culled and modified from this site):

function point(x, y) {
this.x = x;
this.y = y;
}
var point1 = new point(5,-2);
point1.z = 7;

Notice that we’ve added a property to the class “point” in the last line. Although we have only one explicitly defined class in play here, V8 sees the new property and expands the number of hidden classes to two. Clifford would likely recommend doing the following instead:

function point(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
}
var point1 = new point(5, -2, 7);

This will run much more quickly than the above example. We declare a class, constructing an object in accordance with that class, and get the heck out. The number of hidden classes in play remains at one. We should be exercising this kind of vigilance all the time. Which brings me to a related piece of advice:

2. Monomorphic > polymorphic. There may be situations in which changing class properties dynamically turns out to be necessary, but this should be avoided at all costs if possible. The first example I used above is an example of polymorphic coding, in that we’re using multiple hidden classes, probably without even realizing it (unless we already know what’s going on under the hood in Chrome). Conversely, coding as monomorphically as possible means coding parsimoniously and in a way that is attuned to the V8.

4. Careful with your arrays. First of all, how you deal with arrays depends upon their size. If you are dealing with arrays under 65,000 members (Clifford labels these small (!) arrays), you should pre-specify the size of the array, so that the V8 knows how much memory to allocate. And so this

var teams = new Array(2);
teams[0] = “Portland Timbers”;
teams[1] = “Seattle Sounders”;

will end up having a significant speed advantage over

var teams = new Array();
teams[0] = “Portland Timbers”;
teams[1] = “Seattle Sounders”;

The difference here is subtle, but the larger the array you’re working with, the more optimization gains are available. Interestingly, if you’re dealing with “large” arrays (with more than about 65,000 members), it’s actually better to start with an empty array with an unspecified number of members and then build as you go.

Another important piece of advice regarding arrays is related to Clifford’s advice about hidden classes. It’s always better to maintain type consistency across arrays (if possible) instead of mixing and matching data types, as in the array [5, “aardvark”, true, 6.71].

5. Identify the real problem. You should always try to make sure that the problem is really a JavaScript problem and does not reside elsewhere. Because JavaScript is usually tied to the DOM, it is often the case that the problem resides in the JavaScript-DOM relationship and in the general slowness of the DOM rather than in the JavaScript itself. If you’re running benchmarks, try and do so on “pure” JavaScript. This strikes me as a solid piece of more general advice extending beyond V8-oriented optimization.

The Chrome team is still very hard at work on these problems, and the V8 is still very much a work in progress. Clifford admits that there are certain elements of the language that have not yet been optimized, for example try/catch blocks, which Clifford suggests embedding in declared functions rather than letting them dangle on their own.

Following these tips (and others not mentioned here) enabled Clifford to take his non-optimized JavaScript code for calculating the 25,000th prime number from hundreds of times slower than C++ to only 17% slower. As with any and all language benchmarks, take this with a grain of salt: Clifford freely admits that the example was chosen because he knew that it would do well in the V8 engine. But even with the grain of salt dutifully swallowed alongside the presentation, Clifford’s case for the V8 is nonetheless very convincing. I would love to see more side-by-side tests run using less cherry-picked examples.

If I were to venture a few expectations about the V8 project, they would be the following:

1. Over time, more and more elements of the language will be grist for the V8 mill. There is room for improvement, and Google is on the case. The future progress of the V8 engine will likely coincide directly with the rise to dominance of Chrome itself.

2. In a race to keep up, we can most likely expect the other major browsers (IE, Firefox, et al) to embark on their own JavaScript optimization journeys. We’ll have to wait and see if optimizing JavaScript leads to a set of cross-browser best practices, or if the optimization methods in each will diverge in ways that impacts how we optimally code.

This is just a set of first impressions and thoughts. There is much, much more to be learned from the video, and I strongly suggest sitting down and more fully digesting it on your own.

Share this post
Facebook Twitter Google
Try AppFog: The new PaaS Hackers love
  • http://mrale.ph/ Vyacheslav Egorov

    Just adding a property after an object has been constructed does not make your code polymorphic. Polymorphism is an “attribute” of a property access site: the number of hidden classes property access site sees determines if it’s monomorphic (saw only one class, always) or polymorphic (saw more than 1). If you have an expression obj.z somewhere in your code and it only ever sees Points that had z added after construction then that obj.z is still a monomorphic property access; but if it sometimes see a normal Point and sometimes a Point with an additional z — it becomes polymorphic.

  • Luc Perkins

    Yes, you’re right. Thanks for setting me straight. Would you agree, however, with the statement that declaring point1.z = 7 is still a sub-optimal use of monomorphism? It seems to me that there would be speed losses associated with constructing the class that way (especially when it’s unnecessary and could have been done all at once, as in this example).

  • http://mrale.ph/ Vyacheslav Egorov

    I am not sure what you mean by “sub-optimal use of monomorphism”. 
    It is true that in this particular example setting z outside of the constructor will be slightly less efficient than setting z in the constructor, because V8 treats trivial constructors (ones that contain only assignments like this.prop = arg, where arg is one of the parameters) in a special way.

    However once you constructor is non-trivial than there is absolutely no difference in what happens if you set a property inside or outside of constructor. Actions V8 will perform under the hood will be the same.   

  • http://twitter.com/jamonholmgren Jamon Holmgren

    From what I understand, setting the “z” value in your constructor (even to zero or null) will ensure that all of your Point objects will have a “z” value and you won’t have the duplicate hidden class problem.

  • Luc Perkins

    Okay, I think I understand better now. I’m so used to thinking about JS in run-time terms that I was mentally overstepping the entire compiling process and forgetting the distinction. Thanks for your patience with this beginner :)

  • Esailija

    Can you refer where logic for trivial constructors is? I wasn’t aware of it.

    As I understand it, V8 looks at a function syntactically and stores the amount of assignments (“expected number of properties”) done in the function where there is `this.something` on the left-hand side[1]. In my mind it seems any type of expression on the right-hand side would count, not just parameter references. I also don’t think doing other things than assignments in the constructor would affect this.

    Anyway, so you have a count of this assignments done in a function. Then there is additional initial “buffer”, with slack tracking enabled, you get space for 8 additional properties[2]. So objects created from the constructor reserve `assignment count + 8` inobject properties. And after some use, the slack tracking process compacts the map so that inobject property count is as minimal as possible, which is 3 (x, y and z). So it didn’t matter in this case. But if I had defined more than 8 properties outside constructor, then those properties beyond 8 would have gone into properties array and access would be 2 levels of indirection instead of 1.

    Example where this can occur is super classes introducing too many properties:


    function Parent() {
    //Fill the space reserved for 9 inobject properties
    this.prop1 = 1;
    this.prop2 = 2;
    this.prop3 = 3;
    this.prop4 = 4;
    this.prop5 = 5;
    this.prop6 = 6;
    this.prop7 = 7;
    this.prop8 = 8;
    this.prop9 = 9;
    }
    function Child() {
    //"call super constructor" pattern
    Parent.apply(this, arguments);
    //The constructor reserves 1 + 8 = 9 inobject properties
    //The parent call however used them and now our own
    //properties go outside the object
    this.prop = 3;
    }
    function loadInObject( obj ) {
    //All the props added in parent are
    //in object
    //Loads look like
    //mov ecx,[eax+0xb] ;.prop1
    //mov edx,[eax+0x2b] ;.prop9
    return obj.prop1 + obj.prop9
    }
    function loadOutOfObject( obj ) {
    //Sadly our own property added in child went outside
    //Load for the property looks like
    //mov ecx,[eax+0x3]
    //mov ecx,[ecx+0x7]
    return obj.prop + obj.prop;
    }

    [1] parser.cc 2961 https://github.com/v8/v8/blob/b7b9676b75910fdcfd01090f64bc5de563573760/src/parser.cc#L2961

    [2] handles.cc 184 https://github.com/v8/v8/blob/59b676fa72b9d6af60816b030af67f74f08f4c9c/src/handles.cc#L184

  • http://mrale.ph/ Vyacheslav Egorov

    The logic I has been referring to in my comment has since been ripped out. You can see how it looked like in the CL that removed it https://chromiumcodereview.appspot.com/15993016/

    There used to be a separate construct stub for simple constructors and now V8 just relies on Crankshaft to produce good enough code.

    Slack tracking is orthogonal thing but you described it correctly.

    (I apologize for the brevity of my response: I am on vacation without persistent internet connection or even laptop)

Powered by Olark