« Newer | Older »

December 21, 2005

Ajax Perf - comparing the Atlas and Prototype class idioms

I just wrote some perf tests to comapare two different idioms for writing classes in JavaScript, one inspired by Microsoft's Atlas, the other by Prototype.


Classes in Atlas and Prototype

In Atlas, a typical class looks like:
    function Class1() {
        // "public" properties
        this.name = joe";

        // "private" properties (works through closures)
        var _age = 22;
        
        // method
        this.getAge = function() {
            return this._age;
        }
    }
    Class1.prototype = new BaseClass();

For Prototype, you might write:

    Class2 = Class.create();
    Class2.prototype = Object.extend(new BaseClass(), {

        name: "fred",

        initialize: function() {
            this._age = 22;
        },
        
        getAge: function() {
            return this._age;
        }
    });

These two approaches are similar, with two main distinctions:

Its hard to compare these two idioms directly. After all, both are legal JavaScript, so coders can (and do) mix-and-match, adopting different idioms on a per-class or even per-method basis.

I also believe that no performance metric can tell you which technology is right for a particular job. Perf tests are just tables of numbers, nothing more. That said, it is important to understand the performance characteristics of the technology you are using.

Keeping that in mind, lets do some measurements. The source code for these measurements is here.

Tests 0 and 1: The Prototype chain

We start with BaseBaseClass:

    function BaseBaseClass() { }

    BaseBaseClass.prototype.p0 = 10;
    BaseBaseClass.prototype.p1 = 20;

    // inherited method 0
    BaseBaseClass.prototype.testmethod0 = function() {
        return this.p0 + this.p1;
    }

and then define BaseClass:

    function BaseClass() { }

    BaseClass.prototype = new BaseBaseClass();

    BaseClass.prototype.p2 = 10;
    BaseClass.prototype.p3 = 20;

    // inherited method 1
    BaseClass.prototype.testmethod1 = function() {
        return this.p2 + this.p3;
    }

BaseClass and BaseBaseClass are very similar - they both define methods that add two instance properties. BaseClass is derived from BaseBaseClass.

We then define two subclasses, Class1 and Class2. Both derive from BaseClass. Class1 uses the Atlas idiom. Class2 uses the Prototype idiom:

    /* Atlas Style Class */
    function Class1() {
        ...
    }
    Class1.prototype = new BaseClass();

    
    /* Prototype Style Class */
    Class2 = Class.create();
    Class2.prototype = Object.extend(new BaseClass(), {
        ...
    });

To run the tests, we make an instance of Class1 (Atlas) and Class2 (Prototype) and call testmethod0 and testmethod1 a bunch.

Results


Test 0 - BaseBase methodAtlas idiomPrototype idiom
IE6641641
FireFox180180
Opera150140
FireFox/Mac197227
Safari15751587

Test 1 - Base methodAtlas idiomPrototype idiom
IE6481480
FireFox171170
Opera140150
FireFox/Mac262200
Safari15411568

[Numbers are elapsed milliseconds for 100,000 iterations. The first three rows are for a 1.6Ghz Pentium 4 running Windows XP Service Pack 2, FireFox 1.0, Opera 8.51; the last two for a 1.66Ghz PowerBook G4 Mac running OSX Tiger, FireFox 1.0. The absolute figures are not that important - we are interested in relative performance.]

Observations

Test 2: Methods on this vs this.prototype

For this test, we compare this method using the Atlas idiom:

function Class1() {

    this.testmethod2 = function() {
        return this.p2 + this.p3;
    }
    ...

with this from the Prototype idiom:

Class2.prototype = Object.extend(new BaseClass(), {

    testmethod2: function() {
        return this.p2 + this.p3;
    },
    ...

The body of the method is identical to Test1 (both are adding the p2 & p3 properties defined by BaseClass).

Note that Atlas and Prototype versions differ in where the method is stored. With Atlas, the method is set directly on the this instance. With Prototype, the method ends up in this.prototype.

Results

Test 2 - this vs this.prototypeAtlas idiomPrototype idiom
IE6401450
FireFox160160
Opera130140
FireFox/Mac147179
Safari15511576

Observations

Test 3: Properties set directly on this

This test is similar to Test 2, except this time we access properties stored directly on this.

In the Atlas idiom, we use:

    // "public" properties
    this.p4 = 30;
    this.p5 = 40;

    // add properties set directly on the object
    this.testmethod3 = function() {
        return this.p4 + this.p5;
    }

For prototype, we use:

    // public properties    
    initialize: function() {
        this.p4 = 10;
        this.p5 = 20;
    },

    // add properties set directly on the object
    testmethod3: function() {
        return this.p4 + this.p5;
    },

The code is very similar to Test 2 - the only difference is that the two properties being added are set directly on this.

Results

Test 3 - Properties on thisAtlas idiomPrototype idiom
IE6290310
FireFox130130
Opera100120
FireFox/Mac108134
Safari15021525

Observations

Test 4: Closures

Test 4 compares this code using the Atlas idiom:

function Class1 {
    ...
    // "private" properties (works through closures)
    var p6 = 10;
    var p7 = 20;
    
    // properties from the closure
    this.testmethod4 = function() {
        return p6 + p7;
    }
    ...

With this Prototype-style code:

Class2.prototype = Object.extend(new BaseClass(), {
    ...

    // Set properties on prototype
    p6: 30,
    p7: 40,

    // properties from this prototype
    testmethod4: function() {
        return this.p6 + this.p7;
    },

    ...

The difference here is that Atlas is using a closure for private instance variables. The Prototype code is using properties set on the prototype.

Results

Test 4 - ClosuresAtlas idiomPrototype idiom
IE6280360
FireFox130150
Opera100130
FireFox/Mac124166
Safari15381542

Observations

Test 5: Constructors

For this test, we simply construct 10,000 instances of Class1 and Class2.

Results

Test 5 - ConstructorsAtlas idiomPrototype idiom
IE6220150
FireFox1070530
Opera14060
FireFox/Mac367254
Safari1266506

Observations

Test 6: Iterators

One of the crowning features of Prototype is the Enumerable class, which lends JavaScript code a Ruby-like functional style. Although not part of the class definition idiom, it is a major feature of Prototype, so I'll test it too.

In this test, we compare the old style of iterating over an array:

    var b = 0;
    for (var i = 0; i < arr.length; i++) {
        var x = arr[i];
        if (x > 20) break; else b += x;
    } 
    return b;       

with Prototype's functional stype:

    var b = 0;
    arr.each(function(i) { if (i > 20) throw $break; else b += i; });
    return b;

I tried to keep the code within the loop body very similar, so the test is measuring the overhead of the each() and $break mechanism.

The test array we use is:

var arr = new Array(1,2,3, ... ,19,20,21,22);

In other words, the loop iterates over a twenty-item array and then does a break.

Results

Test 6 - IteratorsFor loopFunction
IE61090
FireFox10240
Opera20110
FireFox/Mac52173
Safari316903

(This test run for 1000 iterations)

Observations

Conclusions

In both approaches, there is a penalty for subclassing, especially on IE.

For a performance-critical class, set frequently accessed properties/methods directly on the this instance rather than placing them further up the prototype chain.

The Prototype idiom, generally speaking, produces code that runs marginally slower on IE, when doing general-case method/property access.

The Atlas idiom has the benefit of truly "private" instance variables - you can use closures to define variables that are only visible within a specific method. There appears to be no perf penality for accessing/updating these closure variables.

A major drawback of the Atlas approach is that constructing instances takes much longer. For a class where thousands of instances are constructed (e.g. a IsoDate class), this is a significant issue. For a class that is constructed once and lives the lifetime of the app, this may not be an issue.

The functional iterators of Prototype perform slowly compared to for loops.

Comments


Comments are temporarily disabled. Go to my feedback page to send me comments.