Thursday, May 27th, 2010

Canvas optimization tip: Get image data as infrequently as possible

Category: Canvas, Performance, Tip

<p>We have learned to touch the DOM as little as possible for performance sakes. Batch up changes, and do one call to innerHTML say. Talk over the evil boundary of the DOM as infrequently as possible.

Well, Selim Arsever has found a similar tip for Canvas that caused a ~40% performance improvement on some of his code. He had an example that did pixel twiddling, looking like:

javascript
< view plain text >
  1. canvas = document.getElementById("canvas");
  2. context = canvas.getContext("2d");
  3. image = context.getImageData(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
  4.  
  5. var pixels = SCREEN_WIDTH*SCREEN_HEIGHT;
  6. while(--pixels){
  7.    image.data[4*i+0] = r; // Red value
  8.    image.data[4*i+1] = g; // Green value
  9.    image.data[4*i+2] = b; // Blue value
  10.    image.data[4*i+3] = a; // Alpha value
  11. }
  12. context.putImageData(image, 0, 0);

After listening to Stoyan talk perf, he wondered if there was an issue with the image.data access, and changed the code to:

javascript
< view plain text >
  1. canvas = document.getElementById("canvas");
  2. context = canvas.getContext("2d");
  3. image = context.getImageData(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
  4.  
  5. var pixels = SCREEN_WIDTH*SCREEN_HEIGHT;
  6. var imageData = image.data; // here we detach the pixels array from DOM
  7. while(--pixels){
  8.    imageData[4*i+0] = r; // Red value
  9.    imageData[4*i+1] = g; // Green value
  10.    imageData[4*i+2] = b; // Blue value
  11.    imageData[4*i+3] = a; // Alpha value
  12. }
  13. image.data = imageData; // And here we attache it back
  14. context.putImageData(image, 0, 0);

He wrapped this all up in a benchmark that showed the perf diff. It actually really seemed to matter when using closures for namespaces:

Related Content:

Posted by Dion Almaer at 7:01 am
9 Comments

++++-
4.5 rating from 2 votes

9 Comments »

Comments feed TrackBack URI

Yes, I suppose it should be said: it is established that working with a reference is more efficient than walking an object chain when access is repeated. Hopefully this will save others time doing similar research for particular objects that exist now, or will exist in the future.

Comment by nataxia — May 27, 2010

Is image.data somehow different from normal Javascript object/array’s in that they are not passed by reference? In your code you say detach/and attach, as well as: image.data = imageData; // And here we attache it back (not needed cf. update)

What is cf. update? I ran your test without reassign back and I got similar results, but they were consistently different. Whats reassigning back doing that a shared reference would not?

Comment by atomictim — May 27, 2010

i want to second that my superfluous impression is that the perceived performance gain is due to reduced number of name resolution calls and has per se nothing to do with canvas. it is actually an important and very general technique that works across languages.

i am too lazy to try myself right now, but as pointed out by @atomictim, but unless javascript does some unexpected magic behind the scenes here, you would have to first produce a truly detached copy of the image data array first, manipulate it ‘offline’, as it were, and then feed the data back to image.data. oh, and you actually still need to explicitly putImageData() to update the canvas? not sure about that one either, off of my head.

Comment by loveencounterflow — May 27, 2010

Thanks for the great tip. I don’t know how I managed this kind of thing before…

Comment by sixtyseconds — May 27, 2010

The basic reason for the perf penalty is that browsers implement the DOM as a set of js wrappers around the C/C++ implementations. This means that DOM properties are not JS properties, but rather something closer to a getter. eg. The cached path for “a= foo.data” on an ordinary JS object is something akin to (after the type check):
a = foo->storage[]

whereas for host objects (eg. the DOM) you always fail the type check and go to the slow path which does something along the lines of:
v=foo->hashLookup(“data”)
if (isGetter(v)) a = v()
else a = v

Comment by olliej — May 27, 2010

It’s my experience that some browsers waited till completion of a loop before actually making changes in bulk. I did a line drawing script back in ’98 with some loops and table cell background toggling (don’t look at me like that, I was just starting out). Some browsers would wait till my loops finished then flip all the backgrounds at once, others would draw as the loops worked. Had I known then what I know now I would have done the work first and display when done to get the benefits across all browsers.

Comment by tack — May 27, 2010

Reminds me of the good old Java days. Filling a pixel buffer and repainting the applet. :) Maybe it’s time to brush off those old demo effect and put them back into canvas.

Comment by Spocke — May 27, 2010

@nataxia, @loveencounterflow It may well be the case… I will make a second benchmark using some made-up object instead of the image DOM object and post the result here…

@atomictim In the beginning of the article I say exactly what you say: the array is a reference and doesn’t need to be copied back. In my tests the results are the same without this line but I will make a real benchmark an update you with the results.

@loveencounterflow In my experience you do need to explicitly call the putImageData() function but maybe the behavior vary from one browser to the other.

Comment by selim — May 28, 2010

I can’t help but want to remove those 4*i with (i<<2) but I doubt they have much effect in JavaScript;

I haven't looked into canvas but I'd also be interested to see if you can access it with 32bit ints rather than as bytes to write single ARGB ints rather than separately.

I think the caching of the reference is quite obvious though ;-D

Comment by lightfoot256 — May 28, 2010

Leave a comment

You must be logged in to post a comment.