Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When fine-tuning performance, what is the best way to call JavaScript methods multiple times?

I have been researching JavaScript performance. I've learned that when accessing more than once, it is usually best to copy closure variables and class members into local scope to speed things up. For example:

var i = 100;
var doSomething = function () {
    var localI = i;
    // do something with localI a bunch of times

    var obj = {
        a: 100
    };
    var objA = obj.a;
    // do something with objA a bunch of times
};

I understand this; it adds a shortcut for the interpreter looking up the value by name. This concept becomes very unclear when dealing with methods. At first, I thought it would work the same way. For example:

var obj = {
    fn: function () {
        // Do something
        return this.value;
    },
    value: 100
};
var objFn = obj.fn
objFn();
// call objFn a bunch of times

As it is, this will not work at all. Accessing the method like this removes it from its scope. When it reaches the line this.value, this refers to the window object and this.value will probably be undefined. Instead of directly calling objFn and losing scope, I could pass its scope back into it with objFn.call(obj) but does this perform any better or worse then the original obj.fn()?

I decided to write a script to test this and I got very confusing results. This script makes iterations over several tests which loop through the above function calls many times. The average time taken for each test is output to the body.

An object is created with many simple methods on it. The extra methods are there to determine if the interpreter has to work much harder to locate a specific method.

Test 1 simply calls this.a();
Test 2 creates a local variable a = this.a then calls a.call(this);
Test 3 creates a local variable using YUI's bind function to preserve scope. I commented this out. The extra function calls created by YUI make this way slower.

Tests 4, 5, and 6 are copies of 1, 2, 3 except using z instead of a.

YUI's later function is used to prevent runaway script errors. The timing is done in the actual test methods so the setTimeouts should not effect the results. Each function is called a total of 10000000 times. (Easily configurable if you want to run tests.)

Here's my entire XHTML document I used to test.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xml:lang="en" dir="ltr">
    <head>
        <script type="text/javascript" src="http://yui.yahooapis.com/combo?3.1.2/build/yui/yui-min.js"></script>
        <script>
            YUI().use('node', function (Y) {
                var o = {
                    value: '',
                    a: function () {
                        this.value += 'a';
                    },
                    b: function () {
                        this.value += 'b';
                    },
                    c: function () {
                        this.value += 'c';
                    },
                    d: function () {
                        this.value += 'd';
                    },
                    e: function () {
                        this.value += 'e';
                    },
                    f: function () {
                        this.value += 'f';
                    },
                    g: function () {
                        this.value += 'g';
                    },
                    h: function () {
                        this.value += 'h';
                    },
                    i: function () {
                        this.value += 'i';
                    },
                    j: function () {
                        this.value += 'j';
                    },
                    k: function () {
                        this.value += 'k';
                    },
                    l: function () {
                        this.value += 'l';
                    },
                    m: function () {
                        this.value += 'm';
                    },
                    n: function () {
                        this.value += 'n';
                    },
                    o: function () {
                        this.value += 'o';
                    },
                    p: function () {
                        this.value += 'p';
                    },
                    q: function () {
                        this.value += 'q';
                    },
                    r: function () {
                        this.value += 'r';
                    },
                    s: function () {
                        this.value += 's';
                    },
                    t: function () {
                        this.value += 't';
                    },
                    u: function () {
                        this.value += 'u';
                    },
                    v: function () {
                        this.value += 'v';
                    },
                    w: function () {
                        this.value += 'w';
                    },
                    x: function () {
                        this.value += 'x';
                    },
                    y: function () {
                        this.value += 'y';
                    },
                    z: function () {
                        this.value += 'z';
                    },
                    reset: function () {
                        this.value = '';
                    },
                    test1: function (length) {
                        var time = new Date().getTime();

                        while ((length -= 1)) {
                            this.a();
                        }
                        return new Date().getTime() - time;
                    },
                    test2: function (length) {
                        var a = this.a,
                        time = new Date().getTime();

                        while ((length -= 1)) {
                            a.call(this);
                        }
                        return new Date().getTime() - time;
                    },
                    test3: function (length) {
                        var a = Y.bind(this.a, this),
                        time = new Date().getTime();

                        while ((length -= 1)) {
                            a();
                        }
                        return new Date().getTime() - time;
                    },
                    test4: function (length) {
                        var time = new Date().getTime();

                        while ((length -= 1)) {
                            this.z();
                        }
                        return new Date().getTime() - time;
                    },
                    test5: function (length) {
                        var z = this.z,
                        time = new Date().getTime();

                        while ((length -= 1)) {
                            z.call(this);
                        }
                        return new Date().getTime() - time;
                    },
                    test6: function (length) {
                        var z = Y.bind(this.z, this),
                        time = new Date().getTime();

                        while ((length -= 1)) {
                            z();
                        }
                        return new Date().getTime() - time;
                    }
                },
                iterations = 100, iteration = iterations, length = 100000,
                t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0, t6 = 0, body = Y.one('body');

                body.set('innerHTML', '<span>Running ' + iterations + ' Iterations&hellip;</span>');
                while ((iteration -= 1)) {
                    Y.later(1, null, function (iteration) {
                        Y.later(1, null, function () {
                            o.reset();
                            t1 += o.test1(length);
                        });
                        Y.later(1, null, function () {
                            o.reset();
                            t2 += o.test2(length);
                        });
                        /*Y.later(1, null, function () {
                            o.reset();
                            t3 += o.test3(length);
                        });*/
                        Y.later(1, null, function () {
                            o.reset();
                            t4 += o.test4(length);
                        });
                        Y.later(1, null, function () {
                            o.reset();
                            t5 += o.test5(length);
                        });
                        /*Y.later(1, null, function () {
                            o.reset();
                            t6 += o.test6(length);
                        });*/
                        if (iteration === 1) {
                            Y.later(10, null, function () {
                                t1 /= iterations;
                                t2 /= iterations;
                                //t3 /= iterations;
                                t4 /= iterations;
                                t5 /= iterations;
                                //t6 /= iterations;

                                //body.set('innerHTML', '<dl><dt>Test 1: this.a();</dt><dd>' + t1 + '</dd><dt>Test 2: a.call(this);</dt><dd>' + t2 + '</dd><dt>Test 3: a();</dt><dd>' + t3 + '</dd><dt>Test 4: this.z();</dt><dd>' + t4 + '</dd><dt>Test 5: z.call(this);</dt><dd>' + t5 + '</dd><dt>Test 6: z();</dt><dd>' + t6 + '</dd></dl>');
                                body.set('innerHTML', '<dl><dt>Test 1: this.a();</dt><dd>' + t1 + '</dd><dt>Test 2: a.call(this);</dt><dd>' + t2 + '</dd><dt>Test 4: this.z();</dt><dd>' + t4 + '</dd><dt>Test 5: z.call(this);</dt><dd>' + t5 + '</dd></dl>');
                            });
                        }
                    }, iteration);
                }
            });
        </script>
    </head>
    <body>
    </body>
</html>

I've run this using Windows 7 in three different browsers. These results are in milliseconds.

Firefox 3.6.8

Test 1: this.a();
    9.23
Test 2: a.call(this);
    9.67
Test 4: this.z();
    9.2
Test 5: z.call(this);
    9.61

Chrome 7.0.503.0

Test 1: this.a();
    5.25
Test 2: a.call(this);
    4.66
Test 4: this.z();
    3.71
Test 5: z.call(this);
    4.15

Internet Explorer 8

Test 1: this.a();
    168.2
Test 2: a.call(this);
    197.94
Test 4: this.z();
    169.6
Test 5: z.call(this);
    199.02

Firefox and Internet Explorer produced results about how I expected. Test 1 and Test 4 are relatively close, Test 2 and Test 5 are relatively close, and Test 2 and Test 5 take longer than Test 1 and Test 4 because there is an extra function call to process.

Chrome I don't understand at all, but it's so much faster, perhaps the tweaking of sub-millisecond performance is unnecessary.

Does anyone have a good explanation of the results? What is the best way to call JavaScript methods multiple times?

like image 752
Killthesand Avatar asked Aug 28 '10 21:08

Killthesand


1 Answers

Just theorizing, so take this with a grain of salt...

Chrome's Javascript engine, V8, uses an optimization technique called Hidden Classes. Basicly it constructs static objects that shadow dynamic Javascript objects, where each property/method is mapped to a fixed memory address that can be immeditaly referenced with out the need for an expensive table lookup operation. Every time a Javascript object has a property added/removed, a new hidden class is created.

My theory for the results of your test with Chrome, is that referencing the function in a free local variable breaks the hidden class relationship. While referencing local variables probably also do not require a table lookup, an extra step must now be performed in re-assigning the 'this' variable. For a method on a hidden class, 'this' is a fixed value, so it can be invoked without this step.

Again just theorizing. It might be worth a test to benchmark the difference between local variable references vs object.member references in Chrome, to see if the hit to performance for the latter is signficantly less than in other browsers, presumably because of Hidden Classes.

like image 196
MooGoo Avatar answered Oct 04 '22 02:10

MooGoo