Canvas ImageData pixel manipulation vs Array & Typed-Array + (SetPixel() speed test)

Benchmark created by Unkn0wn on


Description

This tests the cost of calling a SetPixel() function within a rendering loop versus direct array manipulation as well as comparing accessing ImageData against an Array and a Typed-Array. The motivation is to determine efficient javascript rendering for a game engine that performs custom rendering (like JSNes).

The test also compares accessing an ImageArray with Array and Int32Array, where Array (in Chrome) yields the highest performance, though they are all about the same for this experiment.

The results suggest that a fair amount of loop unrolling is still necessary (as of December 2011 on Chrome), but I do expect this area to be improved greatly in the coming years as that can easily be inlined.

One improvement for custom rendering of colours with 1-bit transparency (where alpha values < 255 are considered fully transparent), is to pack them into an ARGB int value. Any alpha value below 255 produces a negative number, which is easier to test than an RGBA value that would require an additional bitwise AND operation. It's a tiny optimization, yes, but worth being aware of for the case of Javascript rendering. Will benchmark this.

Note: Int32Array is used to test typed arrays as an earlier test with Uint8Array was much slower, though it might be worth adding it to the benchmark for comparison sake. Int32Array is used in favour of Uint32Array since it is only concerned with 0-255 range.

Also note:

Preparation HTML

<canvas id="bg0" width="160" height="144">
</canvas>

Setup

var canvas = document.getElementById("bg0");
    var context = canvas.getContext("2d");
    var imageData = context.createImageData(canvas.width, canvas.height);
    const STRIDE_SIZE = canvas.width * 4;
    var arrayData = {
      width: imageData.width,
      height: imageData.height,
      data: new Array(imageData.data.length)
    };
    var s32Data = {
      width: imageData.width,
      height: imageData.height,
      data: new Int32Array(imageData.data.length)
    };
    
    function SetPixel(data, ptr, x, y, r, g, b) {
      data[ptr] = r;
      ++ptr;
      data[ptr] = g;
      ++ptr;
      data[ptr] = b;
      ++ptr;
      data[ptr] = 0xff;
    }
    
    function CreateGradient(imageData, r, g, b) {
      var data = imageData.data;
      var width = imageData.width;
      var height = imageData.height;
      for (var y = 0; y < height; ++y) {
        for (var x = 0; x < width; ++x) {
          var ptr = (x + y * width) << 2;
          var value = ((255 * x / width) + (255 * y / height)) / 2;
    
          var cr = ~~ (value * r);
          var cg = ~~ (value * g);
          var cb = ~~ (value * b);
    
          data[ptr] = cr;
          ++ptr;
          data[ptr] = cg;
          ++ptr;
          data[ptr] = cb;
          ++ptr;
          data[ptr] = 0xff;
        }
      }
    }
    
    function CreateGradientSetPixel(imageData, r, g, b) {
      var data = imageData.data;
      var width = imageData.width;
      var height = imageData.height;
    
      for (var y = 0; y < height; ++y) {
        for (var x = 0; x < width; ++x) {
          var ptr = (x + y * width) * 4;
    
          var value = ((255 * x / width) + (255 * y / height)) / 2;
    
          var cr = ~~ (value * r);
          var cg = ~~ (value * g);
          var cb = ~~ (value * b);
          SetPixel(data, ptr, cr, cg, cb);
        }
      }
    }

Teardown


    for (var i = 0; i < imageData.data.length; ++i) {
      imageData.data[i] = 0xff;
      arrayData.data[i] = 0xff;
      s32Data.data[i] = 0xff;
    }
  

Test runner

Ready to run.

Testing in
TestOps/sec
Direct access (with ImageData)
CreateGradient(imageData, 0xaa, 0x00, 0x00);
ready
Direct access (with Array)
CreateGradient(arrayData, 0xaa, 0x00, 0x00);
ready
Direct access (with Int32Array)
CreateGradient(s32Data, 0xaa, 0x00, 0x00);
ready
Calling a function (with ImageData)
CreateGradientSetPixel(imageData, 0xaa, 0x00, 0x00);
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.

  • Revision 1: published by Unkn0wn on