JavaScript Optimization Patterns (I) by Benedikt Meurer

It’s been a while since I’ve posted on my blog, mostly because I don’t really have the time and energy to sit down and write what I want to write. That’s partly because I’ve been pretty busy with the Ignition translator and TurboFan compiler for Chrome V8 59, which so far has been a huge success. But it’s also partly because I spend some time with my family. Finally, I went to the JSConf EU conference and Web Rebels. As I write this post, I’m still at an enterJS event, putting the finishing touches on my speech.

Meanwhile, I just got back from a dinner with Brian Terlson, Ada Rose Edwards, and ASHLEY Williams. We talked about good optimization patterns in JavaScript, thought about what kind of advice is the least risky to give to others, and specifically talked about how hard it is to come up with these ideas. In particular, I mentioned that ideal performance often depends on the context in which the code is running, and this is often the hardest part, so I thought it might be worth sharing this information with you. This post is the first in a series of posts on my blog to highlight how specific execution environments can affect the performance of JavaScript code.

Consider the following homemade Point class, which has a method called Distance that computes the Manhatten distance between two points.

class Point { constructor(x, y) { this.x = x; this.y = y; } distance(other) { const dx = Math.abs(this.x - other.x); const dy = Math.abs(this.y - other.y); return dx + dy; }}Copy the code

Also, consider the following test driver function, which creates several instances of points, calculates distances between them millions of times, and adds up the results. Ok, I know this is a micro-benchmark, but wait:

function test() {
  const points = [
    new Point(10, 10),
    new Point(1, 1),
    new Point(8, 9)
  ];
  let result = 0;
  for (let i = 0; i < 10000000; ++i) {
    for (const point1 of points) {
      for (const point2 of points) {
        result += point1.distance(point2);
      }
    }
  }
  return result;
}
Copy the code

The Point class, and in particular its distance method, now has a proper reference function. Let’s run the test driver a few times to see how it performs. Use the following HTML snippet:

<script>
    function test() {
        class Point {
            constructor(x, y) {
                this.x = x;
                this.y = y;
            }

            distance(other) {
                const dx = Math.abs(this.x - other.x);
                const dy = Math.abs(this.y - other.y);
                return dx + dy;
            }
        }

        const points = [
            new Point(10, 10),
            new Point(1, 1),
            new Point(8, 9)
        ];
        let result = 0;
        for (let i = 0; i < 10000000; ++i) {
            for (const point1 of points) {
                for (const point2 of points) {
                    result += point1.distance(point2);
                }
            }
        }
        return result;
    }

    for (let i = 1; i <= 5; ++i) {
        console.time("test " + i);
        test();
        console.timeEnd("test " + i);
    }
</script>
Copy the code

If you are running in Chrome version 61 (Canary), you should see the following output in the Chrome Developer tools terminal:

Test 1: 595.248046875 MS test 2: 765.451904296875 MS test 3: 930.452880859375 MS test 4: 994.2890625 MS test 5: 3894.27392578125 msCopy the code

The performance results varied greatly from round to round, and you can see that performance deteriorated over time. The performance deteriorates because the Point class is inside the test function.

<script>
    class Point {
        constructor(x, y) {
            this.x = x;
            this.y = y;
        }

        distance(other) {
            const dx = Math.abs(this.x - other.x);
            const dy = Math.abs(this.y - other.y);
            return dx + dy;
        }
    }

    function test() {
        const points = [
            new Point(10, 10),
            new Point(1, 1),
            new Point(8, 9)
        ];
        let result = 0;
        for (let i = 0; i < 10000000; ++i) {
            for (const point1 of points) {
                for (const point2 of points) {
                    result += point1.distance(point2);
                }
            }
        }
        return result;
    }

    for (let i = 1; i <= 5; ++i) {
        console.time("test " + i);
        test();
        console.timeEnd("test " + i);
    }
</script>
Copy the code

Let’s change the snippet a bit and place the Point class definition outside the test function, and the result is different:

Test 1: 598.794921875 MS test 2: 599.18115234375 MS test 3: 600.410888671875 MS test 4: 608.98388671875 MS test 5: 605.36376953125 msCopy the code

Now, the performance is basically stable, ups and downs are within the normal range. Note that in both cases, the code for the Point class has exactly the same logic as the test driver function. The only difference is where the Point class is placed in the code.

It is also worth noting that this has nothing to do with the new ES2015 class definition syntax. Defining a Point class in the old ES5 syntactic format yields the same performance results:

function Point(x, y) {
  this.x = x;
  this.y = y;
}
Point.prototype.distance = function (other) {
  var dx = Math.abs(this.x - other.x);
  var dy = Math.abs(this.y - other.y);
  return dx + dy;
}
Copy the code

When a Point class is inside a test function, performance differs fundamentally because the class literal variable is executed multiple times. In the example above, the class is defined exactly five times. When the class is outside the test function, the definition is executed only once. Each time a class definition is executed, a new prototype object is generated with all the methods of the class. In addition, a new constructor function corresponding to the class is generated, and that prototype object becomes the “prototype” property of the constructor.

With this “prototype” property as the prototype object, a new instance of the class is generated. But because V8 traces the prototype of each instance, it treats that prototype as part of an Object Shape or hidden class. To optimize access to attributes along the prototype chain, different prototypes naturally mean different shapes of generated objects. Thus, when a class definition is executed multiple times, the generated code becomes more morph. Eventually, V8 saw that there were more than four different object forms and abandoned polymorphism in favor of a state called megamorphic, which meant that it basically stopped generating highly optimized code.

So we know from this exercise that the same code in a slightly different location can easily make a 6.5-fold performance difference! This is extremely important to know. Because common benchmarking frameworks and sites like Esbench.com run your code in a different environment than your own. That is, those sites wrap their code in underlying functions that run multiple times. The resulting benchmark performance test results can be quite misleading.

V8: Behind the Scenes (March edition, emphasis on ignition + turbofan startup and declarative JavaScript)