views:

85

answers:

3

Hello, 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 loosing 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"&gt;
<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"&gt;&lt;/script&gt;
        <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?

+1  A: 

Well, as long as your website has IE8 users as visitors, this is quite irrelevant. Use 1 or 3 (users wont see a difference).

There is probably not a good answer to the "why" question. When it comes to optimization, these script engines are likely to focus on optimizing scenarios that they see happen a lot in real life, where the optimization can be proven to work correct, and where it makes a difference, and in a way that it invalidates the least amount of testing.

jdv
In any real application which has to iterate millions of times, it will probably be calling more complicated functions than value += 'a' and it will probably be calling more than one function per iteration. In this test the optimization of about thirty milliseconds on Internet Explorer may be irrelevant but when the application takes more time to actually do something and that thirty milliseconds gets multiplied by a couple dozen function calls, it starts to add up to very noticeable lag.
Killthesand
+2  A: 

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.

MooGoo
The concept of hidden classes in the V8 engine makes this even stranger. Why is calling this.a() so much more expensive than calling this.z()? Calling a.call(this) is less expensive than calling this.a() but calling z.call(this) is more expensive than calling this.z(). Are they reverse iterating through the members instead of dereferencing memory pointers? I'm wondering if these results are reliable. We're on the scale of 3-5 milliseconds; I could probably move my mouse around the screen and distort these results. I'll try a very large number of iterations on chrome and see how it does.
Killthesand
I've run another test on Chrome, 1000 iterations with a million calls each. It took quite a while to run but provided much more consistent results.Test 1: this.a();168.475Test 2: a.call(this);172.069Test 4: this.z();168.936Test 5: z.call(this);173.012
Killthesand
+1  A: 

Just as a general point, anything you do is unlikely to make a difference if you don't know it is responsible for much time. (By "much" I mean a significant percentage.)

Here's an easy way to find out which code is responsible for much time.

Mike Dunlavey