published on

How to shift your pixels which-way

These days JavaScript is pretty quick but still slower than native code by a good margin. While developing an idea for a visual effect for Vizamp, which I call the shift filter, I needed a level of performance that was unavailable in regular JavaScript.

First let me explain the shift filter. At a basic level it takes the color of each pixel and shifts it from its current array position to another by an offset value. The red, green, blue and alpha components are each given independent offset values, creating the effect of the colors splitting up and moving away from one another. The offsets are parameterized so any number of different kinds of effects can be achieved by passing different values.

While creating the shift filter initially, I quickly realized performance with JavaScript wasn’t cutting the mustard. To give you an idea of the problem, a canvas with 1920 x 1080 pixels has an image buffer with a Uint8Array of 8,294,400 elements. This needs to be iterated over once for each frame when applying the filter, which ideally would be 60 per second (although this is not often achieved). The first attempt at implementing the filter in vanilla JS caused the viz animation to come to a standstill at 1 to 4 FPS and sometimes just froze completely. So I started looking into ways to optimize.

Now, I could have easily created the effect with a shader in WebGL and get the performance I was after, but I wanted to apply the filter to the visualizations I created with the 2D canvas which doesn’t expose WebGL. In my search I stumbled upon asm.js. If you’re not familiar it, asm.js was initially a research project run by Mozilla, and later adopted by other browsers, to create close-to-native-speed performance with JavaScript. What they came up with was a subset of the language that uses an unusual syntax to inform the JavaScript interpreter, ahead of time, how much memory is to be allocated for variables. This allows the interpreter to run the code optimally with far fewer instructions since type conversion is avoided. However useful it is, dynamic typing is extraordinarily expensive.

I won’t make this post an asm.js tutorial, there are better ones a search away, but I will go into it and explain how I got the desired effect and this will give you a little exposure to a technique that you could potentially leverage if you really, really needed to get better performance out of a small section of code.

From the outside, in regular JavaScript land, I get the image buffer data from the canvas then call the filter shift function, which synchronously applies its transformation to the image data with the given the parameters.

const width = ctx.canvas.width;
const height = ctx.canvas.height;
const imageData = ctx.getImageData(0, 0, width, height);

const params = {
  r: -100, g: 100, b: 0, a: 3,
  w: ctx.canvas.width * 4 - 4,
  max: imageData.data.length - (width * 4 * 25)
};

shiftFilter({}, params, imageData.data);

ctx.putImageData(imageData, 0, 0);

The first empty object parameter in the call is where you normally would pass in native libraries to the asm.js function (the weirdness is appearing already). For example the Math library could be passed in, but we’re not using an external library here.

The params object passed in has: - offset values for the colors in properties r, g, b and a - w is the width of the image in terms of the buffer array which has 4 byte elements for each pixel - max is the index of the last array element to be shifted. This is less than the total length of the buffer because we require space to shift into.

Now brace yourself as I unveil the horror that is asm.js:

function pshift(stdlib, foreign, heap) {
  'use asm';
  var w = foreign.w | 0,
    max = foreign.max | 0,
    r = foreign.r | 0,
    g = foreign.g | 0,
    b = foreign.b | 0,
    a = foreign.a | 0,
    i = 0, f = 0;

  for (i = w | 0; (i | 0) < (max | 0); i = (i + 4) | 0) {
    f = (i + w) | 0;
    heap[i << 2 >> 2] = heap[(f + r) | 0 << 2 >> 2];
    heap[((i | 0) + 1) | 0 << 2 >> 2] = heap[(f + g) | 0 << 2 >> 2];
    heap[((i | 0) + 2) | 0 << 2 >> 2] = heap[(f + b) | 0 << 2 >> 2];
    heap[((i | 0) + 3) | 0 << 2 >> 2] = heap[(f + a) | 0 << 2 >> 2];
  }
}

Looks rather esoteric and a bit like that spoof language Brainfuck, doesn’t it? First off, you’ll notice ‘use asm’; neatly nested under the function declaration where you’d usually find ‘use strict’; . This is a decoration to signal the interpreter this function implements optimized asm.js code. At runtime, the interpreter reads ahead of the current execution position and prepares for the optimized code in a process known as Ahead-of-time compilation.

The variable declarations assigning the param values have an unusual postfix:

r = foreign.r | 0

This tells the interpreter an integer type assignment is occurring, similar to the concept of casting. As you look down the code you can see this repeated wherever an interchange or comparison of values occurs. Parenthesis are also used to make types explicit. My code example is actually comparatively straightforward since I’m only dealing with integers.

Accessing a position in the heap array is just as nightmarish with its use of the bitwise operators to offset the position in memory to access array elements:

heap[((i | 0) + 1) | 0 << 2 >> 2] = heap[(f + g) | 0 << 2 >> 2]

But again, this syntax results in type explicit, optimized code for the interpreter.

So, you’re probably thinking, “there’s no way I’m coding in this stuff” and you’d be right. Since asm.js is a total disaster to code in, it was never intended to be a language for developers to actually hand write applications, but rather as a target for compilers, notably Emscripten for compiling C++ for the web.

Furthermore, asm.js is a precursor to a larger concerted effort to provide a performant bytecode interpreter for the web, know as WebAssembly or wasm, currently in development for the major browsers. At some point asm.js will certainly be deprecated and a new generation of compilers and languages that run on the browser will become available. Interesting times ahead for programming language enthusiasts. Will we face a new Cambrian explosion of languages that overwhelm JavaScript or will the 20 year old language hold onto its position as king of the internet? We’ll see. In any case software that traditionally demands high performance, such as video editing and games, will move exclusively to the browser, consigning the humble operating system to a mere shell for running Chrome, Firefox, Edge or whatever your preferred flavor is. I guess Microsoft sees the writing on the wall since they’re starting to wind down Windows releases.

In conclusion, you won’t be writing super performant code any time soon with asm.js, but if you find a nasty little performance bottleneck, especially when processing large typed arrays of data in memory, then it may be worth the time converting that part of your code to asm.js. Just don’t expect to get any help from anyone with it.

Update

Due to feedback on the article, I performed a benchmark test putting asm.js up against normal JavaScript using a scenario inspired by usage described in this article.

You can run the app in your browser from here and the code is available on GitHub here.

Scenario

  1. The test has 2 functions performing an identical operation - one using optimized code and the other using non-optimised code.
  2. Both function perform the shift filter algorithm described above on a Uint8Array which is setup to resemble an image of random color and alpha at 4k resolution - 35,389,440 array elements - 8,847,360 iterations.
  3. The test runs both function in sequence, re-runs them in the reverse order and averages the results.
  4. CPU: 2.6 GHz Intel Core i7
  5. Browser: Chrome 51.0.2704.103

Results In Browser

Starting test - times are in milliseconds
Running asm.js-test-1
asm.js-test-1 complete: 31.765
Running js-test-1
js-test-1 complete: 459.575
Now running the same scenario backwards
Running js-test-2
js-test-2 complete: 475.575
Running asm.js-test-2
asm.js-test-2 complete: 30.27
Test complete
asm.js average 31.0175
js average 467.575
Performance ration js/asm.js = 15.0746

Results In Node.js

Starting test - times are in milliseconds
Running asm.js-test-1
asm.js-test-1 complete: 38
Running js-test-1
js-test-1 complete: 588
Now running the same scenario backwards
Running js-test-2
js-test-2 complete: 591
Running asm.js-test-2
asm.js-test-2 complete: 32
Test complete
asm.js average 35
js average 589.5
Performance ration js/asm.js = 16.8429

Notes: 1. One final note: while setting up the test I discovered that the ‘use asm’; decoration wasn’t actually needed in either the Browser or Node. Apparently the interpreter can read ahead and take the optimized code as-is. 2. I’m using a lower precision method for getting the time when running the test in Node.

Conclusion

As you can see asm.js optimised code performs 15 to 16 times faster than regular JavaScript, at least in this scenario. And with a time of 31ms it’s just about fast enough to squeeze into the 30FPS window on 4K resolution, although that’s assuming you’re not doing something else CPU intensive at the time.