Erik's blog

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

JavaScript performance

, , ,

I work in the core-gfx team here at Opera and are currently working on our WebGL implementation. I've only been with Opera for a little less than two years following a far too long time in the games industry. So far I'm loving every minute in the browser business.

I've been working on a WebGL demo on my spare time for a while and just like I've always been doing when writing games in C++ I wanted to have my assets all neatly wrapped up in a zip archive.

It's very handy, reduces disk footprint, loading time, organizes your files and above all with shell/explorer-integration it's so easy to work with even your most technology ignorant artist, designers and producers can handle it.

Since I couldn't find any javascript library that did that for me I decided to write one myself. I took the excellent C library tinf (tiny inflate) by Jørgen Ibsen, spent a few hours porting it to javascript, a few more to work out why my ported pointer arithmetic didn't work, added a bit of PKZIP archive management source and voila.

It worked like a charm... in Opera. Now, this is where the story takes a turn. This post isn't about my seriously cool WebGL demo, or about how great my little JSUnzip library is, it's about javascript performance and whether Crockford is right in that we're all so busy tuning our browsers to SunSpider and the likes that real-world implementations suffer.

JSUnzip is a straight port of one of the most widely used applications in the world. So if SunSpider, Kraken and V8 are truly indicative of real world performance I'd see the browsers all perform within a reasonable range of each other when unpacking the Canterbury Corpus zip. They didn't. In fact the numbers were so off that I instantly assumed something was wrong with the test. Firefox clocked in at 10 times slower than Opera, Chrome at 60 times slower and Safari at a whopping 100 times slower. (I really tried my best at including IE but it just won't work with XHR and binary data. If you know how to make it, let me know!) When you see results like these you immediately expect them to be wrong, but I both double and triple checked them, verified output, ran it over and over again and asked colleagues to check it out. Intrigued by the results I decided to see if I could zone in on the performance hotspot. The easiest would be the two webkit browsers since they were by far the ones suffering the most. A few hours later I had distilled the inner loop and came up with this test case.

// Hotspot testing. Backreference copy.
var dest = 'apple';
while (dest.length < 100000) {
    var offs = dest.length - 5;
    for (var i = offs; i < offs + 10; ++i)
        dest += dest[i];
}
It's a dead simple inner loop which is something you see in the deflate algorithm. It copies an earlier occurrence of characters from a string and adds it to the end.

You can try the live test-case in your favourite browser here: webkit hotspot test-case.

When I ran it on my box I got these results:
Firefox 4.01         -    8ms
Opera 11.10          -   13ms
Internet Explorer 9  -   26ms
Chrome 11            - 2230ms
Safari 5.05          - 4685ms
Apparently this was not the same hotspot that Firefox hit in the full unzip test as it's soaring through this, but it's spot on for Chrome and Safari. For this dead simple little test we're seeing the fastest browser being more than a factor 500 faster than the slowest one. That's pretty significant. So what does this mean? Not a whole lot in the second browser war at least. I'm sure you could construct more of these where the results would single out another browser. The notable thing though is that my unsuspecting real-world usage hit such a significant difference between browsers when we're currently battling it out in media over milliseconds in SunSpider. Is Crockford right? Maybe. In any instance I don't think it would hurt to revisit the javascript performance test suites.

WebKitters, please optimize that hotspot so I can use JSUnzip in my demo wink and while I'm on the air, kudos to the Opera ES-team for Carakan, Opera's awesome JS engine. As always it's taking names and kicking ass.

Bubuy Flash?

Comments

Martin KadlecBS-Harou Sunday, May 1, 2011 3:46:43 PM

Thanks for jsunzip smile that might be very useful!

António Afonsoantonioafonso Sunday, May 1, 2011 8:19:41 PM

There are actually some JavaScript zip libs out there, for instance: http://jszip.stuartk.co.uk/ from Stuart Knightley who has internship'ed at Opera last year.

Andreas FarreAndreasF Sunday, May 1, 2011 8:40:35 PM

JSZip is nice, but it only does packing, not unpacking which was required in this case.

Joejoelangeway Monday, May 2, 2011 4:31:56 PM

Kudos to the Javascript implementers that managed to make that case fast, but I'd like to point out that it takes non-trivial optimization or trade offs in other areas to make repeated string concatenations fast. That same code, or sequence of operations I guess, would be obviously slow in Java or C#, in C++ it would depend on whose string implementation you chose. The bits constituting the whole string are getting copied every time you add to the end. If you simply avoided doing the character reference, I'm pretty sure V8 in Chrome could optimize this for you and build the string incrementally as you expect. You probably can't avoid that doing an inflate, but you could represent the inflated text as an array of character codes instead of a string perhaps.

Matt Pennigpennig Monday, May 2, 2011 5:09:12 PM

Here's a potential solution. I profiled this code in the latest Safari build, and times went from ~4s to ~90ms.

var dest = 'apple';
while (dest.length < 100000) {
var out = '';
for (var i = dest.length - 5, end = i + 10; i < end; ++i)
out += dest\[i\];

dest += out;
}

Forgive the escaped braces. It tried to turn it into italics.

ithinkihaveacat Monday, May 2, 2011 5:57:26 PM

slice() seems to be much faster than array indexing if you want a sequence of bytes, and the src and dst don't overlap. I've put the original, and splice "solution" (not exactly the same) and Matt Pennig's solution at:

http://jsperf.com/backreference-copy

The variation between engines is quite surprising.

Ilidioilidiomartins Monday, May 2, 2011 6:00:48 PM

Results for Mac OS X

Firefox 4: 12 ms
Opera 11.10: 28 ms
Chrome 11: 2579 ms ~ 2.579 s
Safari 5: 233226 ms ~ 233.226 s

azakai Monday, May 2, 2011 6:31:24 PM

Very interesting performance numbers!

A comment about compressing in JavaScript - you can compile zlib (or any other compression library) from C to JavaScript using Emscripten (in fact zlib is in Emscripten's automatic tests). That might be easier than manually porting a library.

Tom Robinsontlrobinson Monday, May 2, 2011 10:04:52 PM

Another unzip implemenation https://github.com/tlrobinson/zipjs (should be easy to adapt for browser use)

Erik Mölleremoller Tuesday, May 3, 2011 6:57:11 AM

Thanks for all the comments. Like people pointed out here, in emails, twitter and what I found myself when analyzing this further is that there are plenty of optimizations that can be done to jsunzip. Using slice when possible (btw Matt Pennings solution above is not performing the same operations as the initial test case. There's a subtle difference in that the original copies data it recently added itself in the same iteration) or using arrays instead of strings. Using arrays does give you a hit on memory usage during uncompression on most browsers, I'm guessing somewhere in the range of x1 - x8, but that may be well worth it for the performance boost.
In any instance, the fact that there was such a huge difference in performance on something as elementary as string concatenation, something seen in just about every javascript out there today, was intriguing and I think the post served its purpose well and sparked a lot of interesting discussions... hopefully making the interweb a better place.

LennieSilentLennie Sunday, May 8, 2011 8:07:54 PM

Paul Rouget (Mozilla) had also had a look at compression and javascript already:

http://twitter.com/#!/paulrouget/status/26961510848790528

He found these:
http://blog.renevier.net/index.php?post/2011/01/07/js-library-to-read-zip-file
http://fhtr.blogspot.com/2010/05/loading-targz-with-javascript.html
http://fhtr.blogspot.com/2010/05/parsing-tarballs-with-javascript.html

Also in Firefox you can also try their JavaScript extension for handling binary data to see how much extra performance that will get you:

https://developer.mozilla.org/en/JavaScript_typed_arrays

Have a nice day.

ms7821 Sunday, May 8, 2011 8:40:02 PM

I'm a bit surprised that you're indexing characters like this. This kind of string concatenation loop is why StringBuilders exist.

I'm also not sure arrays help that much. By far the fastest method is substring, which presumably translates directly into the OS string/character copy function.

Tests here:

http://jsperf.com/emollerweird

Remy Sharpremysharp Sunday, May 8, 2011 9:55:26 PM

I read recently that string concatenation was only slow in IE nowadays. I only noted it because when I'm building up big(ish) strings, I tend to create an array, push to the array and join it then creating a string. However, like I said, I read some performance stats on how only IE was slow at that string concatenation.

From what I can see, it's just the speed increase that Webkit has had over the years that's simply masking, what appears to be the same problem.

Basically, I changed your hotspot testing to use an array instead of concatenation and performance gets lots better - i.e. Safari down from 4seconds to 7ms, and Chrome down from 2.5seconds to 10ms - really the way it should be.

Working example: http://jsbin.com/aruze3 (source: http://jsbin.com/aruze3/edit )

Basically, instead of the var dest = 'apple', I use var dest = 'apple'.split('') and instead of dest += dest, I use dest.push(dest);

Then once you're done, a one hit puts the string back together via dest = dest.join('');

This simply proves that string concatenation is just as crap as it's always been, only to, browsers got faster, so the problem appears to go away. Under the hood it may be something entirely different - but this sure smells like the old school concat issues.

Charles SchlossChas4 Sunday, May 8, 2011 11:04:01 PM

Tho Safari and Chrome both have webkit base they have different javascript engines

Remy Sharpremysharp Sunday, May 8, 2011 11:49:08 PM

Damn, after almost being able to sleep on this, I more and more think the problem isn't the concat, but the index lookup - which is what's been already discussed. Obviously the array provides the solution because it has the native index support - whereas swapping data out for data.substr(i, 1) yields almost exactly the same results.

Rod Knowltoncodelahoma Monday, May 9, 2011 12:06:53 AM

Definitely the indexed lookup in this case. Here's Erik's example, modified to use an index into a predefined source array, rather than into the destination string itself: http://jsbin.com/codelahomahotspot/2

Seems odd that an indexed reference into a string isn't just aliased to substr(i,1), though.

Am I missing a reason why it wouldn't be implemented as such?

Sam Lalanisamlalani Thursday, May 12, 2011 3:35:49 AM

I don't know if I'm off-topic but I have a Javascript speed issue with the following code running in Opera 11 on Xoom and on Win7. It is really slooooow compared to other browsers.

function getRect (object)
{
var left = object.offsetLeft;
var top = object.offsetTop;
var width = object.offsetWidth;
var height = object.offsetHeight;
return ({x:left, y:top, w:width, h:height});
}

Erik Mölleremoller Friday, May 13, 2011 1:13:36 PM

Hi Sam, the best way of making sure a hotspot gets some attention by the browser vendors ES teams would be to create a small testcase on http://jsperf.com/ or some similar site and point us toward it.

Write a comment

New comments have been disabled for this post.