Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is calling a method on an object literal slower on V8?

I was surprised by the results of this simple jsperf test:

Benchmark.prototype.setup = function() {
  var O = function() {
      this.f = function(){};
  }
  var o = new O();
  var o2 = {
      f : function(){}
  };
};

// Test case #1
o.f();  // ~721M ops/s

// Test case #2
o2.f(); // ~135M ops/s

I expected both to perform the same (and in fact the performance is similar in Firefox). V8 must be optimizing something on case #1, but what?

like image 288
bfavaretto Avatar asked Feb 27 '14 02:02

bfavaretto


2 Answers

First fundamentals about V8 and jsPerf:

  • V8 uses an technique called hidden classes. Each hidden class describes a certain object shape e.g. object has property x at offset 16 or object has method f and these hidden classes are connected together with transitions as the object is mutated forming transition trees (which are strictly speaking dags). Not all hidden classes reside in the same transition tree; instead a new transition tree is born from each constructor. Take a look at these slides to grasp the basic idea behind hidden classes.

  • When jsPerf does the following to run your test: given setup and body it multiple times generates and runs a function looking approximately like this:

    function measure() {
      /* setup */
      var start = Date.now();
      for (var i = 0; i < N; i++) {
        /* body */
      }
      var end = Date.now();
    
      /* N / (start - end) determines ops / ms reported */
    }
    

    Each run is called a sample.

Now lets take a look at transition trees in your benchmark.

  1. Hidden class of o belongs to the transition tree with a root at constructor O. Notice that each constructor is invoked a single time. This allows V8 to build the following transition tree in memory:

    O{} -f-> O{ f: <closure> }
    

    The hidden class of o essentially tells V8 that o has a method called f implemented by given closure. This allows a V8's optimizing compiler to inline f in your benchmark above which essentially makes benchmarking loop empty.

  2. Hidden class of o2 belongs to the transition tree of Object. First notice that setup is invoked multiple times so if V8 tried to apply the same optimization with promoting f to a method it would arrive to an impossible transition tree:

    Object{} -f-> Object{ f: <closure> }
             -f-> Object{ f: <other closure> }
    

    In fact, V8 does not even try. V8 implementors foresaw this situation and V8 just makes f a normal property:

    Object{} -f-> Object{ f: <property at offset 8> }
    

    Thus to invoke o2.f() it needs to load it first and this also impairs inlining.

Here you should realize one important thing: if you invoke O constructor twice then V8 will arrive to the same impossible transition tree that V8 avoids hitting for Object:

    O{} -f-> O{ f: <closure> }
        -f-> O{ f: <other closure> }

You can't have structure like that. In this case V8 on the fly converts f to a field instead of making it a method and rewrites transition tree:

    O{} -f-> O{ f: <property at offset 8> }

See this effect in http://jsperf.com/function-call-on-js-objects/3 where I added one new O() before you create o. You will notice that the performance of object literal and object constructed with new are the same.

Another detail here is that V8 would try to turn f into a method for the literal as well, if the literal is declared in the global scope. See http://jsperf.com/function-call-on-js-objects/5 and Issue 2246 against V8. The reasoning here is simple: literal in the global scope is evaluated only once so it's likely that such promotion will succeed and there will be no clashes between methods that would arise if literal is evaluated multiple times.

You can read more about similar issues in my blog post.

like image 68
Vyacheslav Egorov Avatar answered Nov 01 '22 05:11

Vyacheslav Egorov


V8 makes optimizations for known prototypes. In other words, usage and creation of objects via new is optimized.

You can write more similar tests and this will always be the conclusion.

In the second case, you're blinding the engine. It doesn't know why, if or when o2 will have an attribute.

like image 37
slezica Avatar answered Nov 01 '22 06:11

slezica