Erik's blog

http://twitter.com/#!/erikjmoller

JavaScript performance 2

, , ,

I got an incredible response to my first post about JavaScript performance, both good and bad. Some people seemed almost annoyed that I hadn't optimized the jsunzip code further to get around the webkit hotspot, but I think it's safe to say they missed my point. When you port something you generally do as simple a port as possible, get it running and THEN look at optimizations. The thing that struck me was the huge difference in performance between browsers in the unoptimized version and the right thing to do is to report it (even if it makes your browser look bad) instead of swiping it under the carpet and push on with the optimizations.

In any instance, I think it stirred up a lot of good discussions and now I'd like to continue to report a bit on some later findings now that I have spent a few hours optimizing jsunzip.

Before I move on to that though I'd like to post a piece of code here. It was really annoying me that I couldn't get junzip (or rather binary XHR) to run with Internet Explorer, leaving a mere 45% of all the browsers out, so I decided to investigate it once and for all. I had a hard time to find any info on it on the net, but eventually I managed to get it going. Here's how you get binary XHR to work with IE9:

function useMSXHR() {
    return typeof ActiveXObject == "function";
}

function getBinaryFile(url, callback) {
    var request = useMSXHR() ? new ActiveXObject("Msxml2.XmlHttp.6.0") : new XMLHttpRequest();
    request.onreadystatechange = function() {
        if (request.readyState == 1) {
            if (request.overrideMimeType) {
                request.overrideMimeType('text/plain; charset=x-user-defined');
            }
            request.send();
        }

        if (request.readyState == 4) {
            if (request.status == 200) {
                var data;
                if (useMSXHR()) {
                    var data = new VBArray(request.responseBody).toArray();
                    for (var j = 0; j < data.length; ++j)
                        data[j] = String.fromCharCode(data[j]);
                    callback(data.join(''));
                    request.abort();
                } else {
                    callback(request.responseText);
                }
            } else {
                // Report error
            }
        }
    }
    request.open("GET", url, true);
}
Note that you might also want to check for Msxml2.DOMDocument.3.0 as a fallback, but I didn't include that here. It's pretty darn simple, I wish it would've been easier to come by. Hopefully this will come up for at least a few people trying to google this next.

One more thing before we dive into the results. Kind browser reviewing people of the Internet, how about this? It sort of makes labels redundant. Ok, silver-grey for Safari could've been clearer, but I'm afraid IE won the battle for the blue bar by a wide margin. All right, let's get on with the optimizations. The changes I made to speed up jsunzip across the browsers included moving away from the string concatenation webkit hotspot by using arrays instead and rewriting some of the inner loops in a more javascript friendly way. Surprisingly things like the snippet below still makes quite a difference. I say surprisingly because I'm used to working with C where the compiler does a great job, but when you think about getters and setters and class checks needed for each property lookup it makes sense with these kind of optimizations, helping the compiler with some subexpression elimination. I wish I wouldn't have to, but I wish a lot of things. Maybe one day.
// js optimization.
var ddest = d.dest;
while(...) {
    ...
    ddest.length++;
}

The hotspot inner loop I reported on last time now uses arrays and looks like this.
/* copy match */
for (i = offs; i < offs + length; ++i) {
    ddest[ddestlength++] = ddest[i];
}

Another hotspot was the read_bits() which operated on the bits one at a time in the old implementation. Rewriting it to fetch more data at a time and getting all the bits out at once obviously made quite the difference. It now looks like this.
/* read a num bit value from a stream and add base */
this.read_bits = function(d, num, base)
{
    if (!num)
        return base;

    var val = 0;
    while (d.bitcount < 24) {
        d.tag = d.tag | (d.source.charCodeAt(d.sourceIndex++) & 0xff) << d.bitcount;
        d.bitcount += 8;
    }
    val = d.tag & (0xff >> (8 - num));
    d.tag >>= num;
    d.bitcount -= num;
    return val + base;
}


They're all pretty simple rewrites, but doubled the performance across the board on the five browsers. So, what were the results?
Opera 11, Firefox 4, Safari 5 and Chrome 11 are all within a 5% margin of each other, but IE9 has roughly a 25% lead, currently decompressing about 10MB/s on my core-2-duo.
The optimized source of jsunzip can be found at its google project hosting page.
This post would of course not be worth much without the performance test itself. Apologies for having to gray out the "unzip and verify" but there were some logistical issues on the blog. The full test is available for download on the google code page. Try the JSUnzip performance test for yourself.

Bubuy Flash?Emberwind reborn in HTML5

Comments

Karl Dubostkarlcow Friday, May 13, 2011 5:17:54 PM

The build version for each browser would help too and on which platform.

On macosx 10.6.7, 10 tries of unpack.zip

* Opera Next (11.50 alpha, 1009) - 427ms to 442ms
* Opera (11.10, 2092) - 431ms to 446ms
* Firefox (4.0.1) - 338ms to 409ms
* Aurora (5.0a2 2011-05-05) - 294ms to 361ms
* Safari (5.0.4 6533.20.27) - 289ms to 299ms

almacdonald Tuesday, November 29, 2011 4:55:03 AM

This code did not work in IE9 for me. I just get the response text instead. Is there some other code you need to add to get the mime type override to kick in?

almacdonald Tuesday, November 29, 2011 5:28:04 AM

Actually using charCodeAt() in Firefox, Chrome, Safari; then using fromCharCode() as you have done in IE, I can get the same data, except the start byte is different.

In IE/Opera the value of my start byte is: 137 and in Chrome/Firefox/Safari it is: 63369. (Max val of a UNIT8?)

Either way.... it's use-able.

Pretty cool!!

Thanks for the snippet Erik.

almacdonald Tuesday, November 29, 2011 7:16:31 AM

Hmm... looks like any value in the IE toArray() that is above 128 gets lost in IE.

MyOpera team, please fix this!fearphage Monday, January 7, 2013 6:38:34 PM

Another way to speed things up is `String.fromCharCode` takes any amount of arguments.

So that loop can be removed and replaced with:

if (useMSXHR()) {
  callback(String.fromCharCode.apply(null, new VBArray(request.responseBody).toArray());
  request.abort();
}


Write a comment

New comments have been disabled for this post.