Going Native With Asm.js Lightning Talk, March 29, 2015

The following is a transcript of a talk I gave the the Denver HTML5 Users Group at Rally Software in downtown Denver on Monday, March 23, 2015

The fractal-rendering software demonstrated during this talk lives here:

Daniel: My name is Daniel Langdon. I've been an IT consultant here in Denver for the past couple years and Denver's been really great to me, and I've been looking forward to this opportunity to give a little bit back, so I'm going to be giving you a talk about asm.js, but since I don't have time to do a super deep-dive into the code right now, I'd like to start out by telling you about my motivation to be an early adopter of this system that you may have never heard of. So to begin...

Mandelbrot Fractal defined by iterating function
f(z,c) = z2 + c

Who can tell me what this is?

Audience members: Fractal! Mandelbrot!

Daniel: Right, the Mandelbrot Fractal! It was discovered by the late Benoit Mandelbrot who passed away just a few years ago. He pointed out that this figure, in its amazing complexity, is actually defined by extremely simple mathematics. In a PBS interview, he said, "As a matter of fact, all you have to do is add and multiply; you don't even have to subtract and divide!" Why then, was this not discovered by Pythagoras or Archimedes?

Single audience member: You have to do a lot of it!

Right! To do a figure like this, you must do the additions and multiplications billions and billions of times. This software is something that I wrote in my spare time, just out of an interest in HTML5. I was reading a book; they were talking about the Canvas element, the Web Workers so you can parallelize your computations and everything, but the elephant in the room to me is that while we have all this great new stuff in HTML5 to let us draw however we want, do parallel computations, do video, audio, and all of that fun stuff, we're still doing all of our computations in Javascript, which for all the wonderful things about it and I think it's a great language, it's a dynamic, interpreted language that uses duck-typing, so if you're doing something like billions and billions of additions and multiplications of floating-point numbers, it's probably not the best solution, but if you're working in the browser, that's what you're stuck with, and asm.js fills in that void.

This started out as an experimental feature in FireFox. I thought perhaps it would be a flash in the pan; maybe they wouldn't find the killer app for it. I wish that instead of showing you this, I could show you the Unreal Citadel demo, which I'm told is a port of an Unreal demo, a game engine, that lets you walk around a castle in 3D in a web browser, but unfortunately, they took it down and replaced it with something called "Tappy Chicken", which isn't nearly as good. I have no idea why they did that, but the good news is, just last month, even after I had signed up to give this talk, Microsoft has announced that they are going to put support for asm.js in the latest iterations of their browser stack and not only that, but Google is, too, I think, and Microsoft also announced that support for asm.js was in the top 10 list of features requested for Internet Explorer.

If you want to know exactly what this is, asm.js is not a library like the name suggests like prototype.js or angular.js. What it is is a C-like language embedded inside Javascript, and the genius of that is that when a browser that doesn't know asm.js encounters the code, it can still run the code, albeit slowly. If it's something like the Unreal Citadel demo, it's going to be impossible to use, but if we're doing something like this, you can draw these pictures with Internet Explorer, it's just going to be really, really slow.

function getComputationModule() { var computationModule = (function foo1(stdlib, foreign, heap) { "use asm"; var sqrt = stdlib.Math.sqrt, sin = stdlib.Math.sin, cos = stdlib.Math.cos, atan = stdlib.Math.atan, atan2 = stdlib.Math.atan2, exp = stdlib.Math.exp, ln = stdlib.Math.log, floor = stdlib.Math.floor, ceil = stdlib.Math.ceil, heapArray = new stdlib.Int32Array(heap), outR = 0.0, outI = 0.0; function computeRow(canvasWidth, canvasHeight, limit, max, rowNumber, minR, maxR, minI, maxI) { canvasWidth = +canvasWidth; canvasHeight = +canvasHeight; limit = +limit; max = max | 0; rowNumber = +rowNumber; minR = +minR; maxR = +maxR; minI = +minI; maxI = +maxI; var columnNumber = 0.0, zReal = 0.0, zImaginary = 0.0, numberToEscape = 0; var columnNumberInt = 0; // Compute the imaginary part of the numbers that correspond to pixels in this row. // This computation takes into account the the imaginary value goes *down* as the y-coordinate on the canvas increases. zImaginary = maxI - (((maxI - minI) * +rowNumber) / +canvasHeight); // Iterate over the pixels in this row. // Compute the number of iterations to escape for each pixel that will determine its color. for (columnNumber = +0; +columnNumber < +canvasWidth; columnNumber = +(+columnNumber + 1.0)) { // Compute the real part of the number for this pixel. zReal = +(((maxR - minR) * +columnNumber) / +canvasWidth + minR); numberToEscape = howManyToEscape(zReal, zImaginary, max, limit) | 0; columnNumberInt = columnNumberInt + 1 | 0; heapArray[(columnNumberInt * 4) >> 2] = numberToEscape | 0; } } // Function to determine how many iterations for a point to escape. function howManyToEscape(r, i, max, limit) { r = +r; i = +i; max = max | 0; limit = +limit; var j = 0, ar = 0.0, ai = 0.0; ar = +r; ai = +i; for (j = 0; (j | 0) < (max | 0); j = (j + 1) | 0) { iteratingFunction(ar, ai, r, i) ar = outR; ai = outI; if (+(ar * ar + ai * ai) >= +(limit * limit)) return j | 0; } return j | 0; } // This file is loaded via AJAX and the string below will be replaced with an actual iterating function. // The iterating function defining the fractal to draw // r and i are the real and imaginary parts of the value from the previous iteration // r0 and i0 are the starting points // It is expected that this file will actually be fetched by AJAX at which point, // the string below, will be replaced with code to implement // a particular iterating function. "ITERATINGFUNCTION" // The use of eval would probably give Douglas Crockford heart palpitations :-) // Truncates the decimal part of a real number. function truncateDecimal(r) { r = +r; if (+r > 0.0) return +floor(r); else return +ceil(r); return 0.0; } // Compute the result of [r,i] raised to the power n. // Right now, this only supports whole numbers, but the calling code uses only doubles, so that's what it's declared as. // Place the resulting real part in outR and the imaginary part in outI. function computePower(r, i, expr, expi) { // Tell asm.js that r, i are floating point and n is an integer. r = +r; i = +i; expr = +expr; expi = +expi; // Declare and initialize variables to be numbers. var rResult = 0.0; var iResult = 0.0; var j = 0.0; var tr = 0.0; var ti = 0.0; // Declare and initialize variables that will be used only in the // event we need to compute the reciprocal. var abs_squared = 0.0; var recr = 0.0; var reci = 0.0; if (+truncateDecimal(expr) == +expr) if (expi == 0.0) { if (+expr < 0.0) { // For n less than 0, compute the reciprocal and then raise it to the opposite power. abs_squared = +(r * r + i * i); recr = +r / abs_squared; reci = -i / abs_squared; r = recr; i = reci; expr = +(-expr); } rResult = r; iResult = i; for (j = 1.0; +j < +expr; j = +(j + 1.0)) { tr = rResult * r - iResult * i; ti = rResult * i + iResult * r; rResult = tr; iResult = ti; } outR = rResult; outI = iResult; return; } // If the exponent is not a whole number or has non-zero imaginary part, use logarithms // together with the exponential function to compute the power. // x ^ y = e ^ (ln(x) * y) // Compute the natural log of the base: compute_ln(r, i); // Multiply that by the exponent: multiply(outR, outI, expr, expi); // Exponentiate the result compute_exp(outR, outI); // The result is now in outR, outI. } // end computePower function add(r0, i0, r1, i1) { r0 = +r0; i0 = +i0; r1 = +r1; i1 = +i1; outR = +(r0 + r1); outI = +(i0 + i1); } function subtract(r0, i0, r1, i1) { r0 = +r0; i0 = +i0; r1 = +r1; i1 = +i1; outR = +(r0 - r1); outI = +(i0 - i1); } function multiply(r0, i0, r1, i1) { r0 = +r0; i0 = +i0; r1 = +r1; i1 = +i1; outR = r0 * r1 - i0 * i1; outI = r0 * i1 + r1 * i0; } function divide(r0, i0, r1, i1) { r0 = +r0; i0 = +i0; r1 = +r1; i1 = +i1; outR = +(((r0 * r1) + (i0 * i1)) / (r1 * r1 + i1 * i1)); outI = +(((i0 * r1 - r0 * i1)) / (r1 * r1 + i1 * i1)); } function compute_real(r, i) { r = +r; i = +i; outR = +r; outI = 0.0; } function compute_imag(r, i) { r = +r; i = +i; outR = 0.0; outI = +i; } function compute_abs(r, i) { r = +r; i = +i; // If the number is purely real, no need to compute square roots. if (i == 0.0) { outR = +(+r > 0.0 ? +r : -r); outI = 0.0; } else { outR = +sqrt(r * r + i * i); outI = 0.0; } } // Compute the "Argument" of a complex number, that is the angle of the number in polar coordinates. function compute_arg(r, i) { r = +r; i = +i; if (r == 0.0 & i == 0.0) { // Although arg(0) is undefined, I will use 0 here to avoid errors. outR = 0.0; outI = 0.0; } else { // outR = +(2.0 * +atan(i / (+sqrt(r * r + i * i) + r))); outR = +(atan2(i, r)); outI = 0.0; } } // Compute the conjugate of a complex number. function compute_conj(r, i) { r = +r; i = +i; outR = +r; outI = +(-i); } // Compute the sine of a number given its real and imaginary parts. function compute_sin(r, i) { r = +r; i = +i; outR = +(+sin(r) * (+exp(i) + +exp(-i)) / +2); outI = +(+cos(r) * (+exp(i) - +exp(-i)) / +2); // // This is an experiment to see if using the Taylor series is faster in asm.js // var powerR = 0.0; // var powerI = 0.0; // var factorial = 1.0; // var multiple = 1.0; // var z2_r = 0.0; // var z2_i = 0.0; // var a_r = 0.0; // var a_i = 0.0; // var j = 0.0; // // z ^ 2 // multiply(r, i, r, i); // z2_r = +outR; // z2_i = +outI; // // accumulator // a_r = +r; // a_i = +i; // for (j = 1.0; // +j < 10.0; // j = +(j + 1.0)) { // factorial = +(factorial * (j * 2.0) * (j * 2.0 + 1.0)); // multiply(powerR, powerI, z2_r, z2_i); // powerR = +outR; // powerI = +outI; // outR = +outR / factorial; // outI = +outI / factorial; // multiple = +multiple * -1.0; // add(a_r, a_i, outR * multiple, outI * multiple); // a_r = +outR; // a_i = +outI; // } } function compute_sh(r, i) { r = +r; i = +i; // Compute hyperbolic sine using the formula below. // sinh(x) = -i * sin(i * x) multiply(r, i, 0.0, 1.0); compute_sin(outR, outI); multiply(outR, outI, 0.0, -1.0); } function compute_cos(r, i) { r = +r; i = +i; outR = +(+cos(r) * (+exp(i) + +exp(-i)) / +2); outI = +(-(+sin(r)) * (+exp(i) - +exp(-i)) / +2); } function compute_ch(r, i) { r = +r; i = +i; // cosh(x) = cos(i * x) multiply(r, i, 0.0, 1.0); compute_cos(outR, outI); } // Compute the natural exponental for a number given its real and imaginary parts. function compute_exp(r, i) { r = +r; i = +i; var t = 0.0; t = +exp(+r); outR = +(t * +cos(i)); outI = +(t * +sin(i)); } // Compute the natural log for a number given its real and imaginary parts. // ln(a+bi) = ln(abs(z)) + i * arg(z) function compute_ln(r, i) { r = +r; i = +i; var realPart = 0.0, imagPart = 0.0; compute_abs(r, i); realPart = +ln(outR); compute_arg(r, i); imagPart = +outR; outR = +realPart; outI = +imagPart; } function get_outR() { return +outR; } function set_outR(r) { r = +r; outR = +r; } function get_outI() { return +outI; } function set_outI(i) { i = +i; outI = +i; } return { // The primary point-of-entry for the computation module computeRow: computeRow, // These functions are exposed to make them testable, but they won't normally be directly invoked. computePower: computePower, add: add, subtract: subtract, multiply: multiply, divide: divide, compute_real: compute_real, compute_imag: compute_imag, compute_abs: compute_abs, compute_arg: compute_arg, compute_conj: compute_conj, compute_sin: compute_sin, compute_cos: compute_cos, compute_sh: compute_sh, compute_ch: compute_ch, compute_exp: compute_exp, compute_ln: compute_ln, get_outR: get_outR, set_outR: set_outR, get_outI: get_outI, set_outI: set_outI }; })(self, foreign, heap); // Return computationModule that we just defined. return computationModule; }
View on GitHub

Now that I've given you the general overview of what it is and what you can do with it, I'd like to show you a little bit of the code to round out my presentation this evening. What we have here is some asm.js code. I didn't do any code for the user interface or clicking the buttons or that sort of thing in asm.js; only the mathematics. One question that probably comes to your mind is, "Since Javascript is duck-typed, how do you embed stronly-typed C code in the language?", and here's how: this is a function I'm using as an example called "computeRow". ::pauses to make code appear bigger on display:: Here we have a function called computeRow; it has a number of parameters that are passed in; regular Javascript code, and this is really just skimming the surface of how asm.js syntax works. At the very beginning of the function, we assign each parameter to itself in such a way to perform an operation that would, in ordinary Javascript, convert the parameter to that type. For example, the first three, "canvasWidth", "canvasHeight", and "limit" all say "parameter = +parameter" and that unary plus normally just tells Javascript that something is a number, and if it isn't a number, it converts it, but since it is a number, it does nothing. When asm.js sees this, it says "Okay, this is a function, and these first three parameters are 64-bit floating-point numbers." The fourth one says "max = max | 0". Or is a weird operator in Javascript; it normally uses 64-bit floating-point numbers for everything, but with the "|" operator, both operands are converted to a 32-bit integer, so when you say, "max = max | 0", it just has the effect of converting it to a 32-bit integer, because any integer | itself [This is an error; I was referring to the integer | 0.] is the integer you started out with, so a standard Javascript interpreter is going to run thru that and think, "This is stupid! What is this for?" It's a special syntax, but when asm.js sees this, the magic starts happening!

There is a tool for generating asm.js code, which I actually did not use for this particular project, but I just wanted to point out. It's called Emscripten. It converts C or C++ code to Javascript and it normally targets asm.js. ::spells E-M-S-C-R-I-P-T-E-N and displays it on screen:: One thing I've read that Emscripten does which I think is very amazing is, you'll see here at the beginning of the asm.js code, you declare everything inside a function which is marked with "use asm" like "use strict". We have three parameters: "stdlib", "foreign", and "heap". stdlib gives you access to the standard functions, like the math functions. foreign allows you to pass in non-asm.js code to interface with it, and then there is heap, which functions like an array. The magical thing about heap is that Emscripten does some magic under the covers when you allocate memory in your C code to use that array to simulate memory; it's completely backwards-compatible with regular Javascript code, but it goes into that paradigm of being able to compile it in one browser and interpret in a browser that doesn't support it.

Mandelbrot Fractal defined by iterating function
f(z,c) = sin(z/c)

I actually wrote my own little code-generation tool which, since you may know that these images are defined in terms of mathematical functions, I wrote code that interprets what we think of as standard mathematical language, so I can type in a completely different mathematical function, and I know this function generates a beautiful plot, so just to illustrate how it works, I'm going to change the function from "z2 + c", we're going to draw a plot for "sin(z/c)", and you can see that this one takes a good bit of time to load even with asm.js, but it draws a completely different image. This is just one small example of the power you get with asm.js to fill in the void left by having to deal with a dynamic language for doing heavy-duty mathematics.

That's my talk. Are there any questions?

Audience member: Is that C language, or the version you show being compiled?

Daniel: Yes. The tool Emscripten I talked about takes C code and converts it to Javascript code that is compatible with asm.js. I took a hybrid approach; I wrote some asm.js functions by hand which is normally not encouraged, and then I wrote code that would convert mathematical notation into a series of invocations of those functions and variable assignments. Does that answer your question?

Audience member: Sort of; so you're going thru several layers, then, but ultimately, you're compiling some code, creating more speed?

Daniel: Correct, so ultimately, the browser receives Javascript code in this special asm.js syntax, and if the browser supports asm.js, the browser compiles it; otherwise, it gets treated as any other Javascript code.

Audience member: How can you know for sure if the browser is running it in the optimized asm.js mode or not?

Daniel: That's a great question. In the development console in Mozilla FireFox, it will show you a compilation error if it didn't work, and believe me, I've seen lots of them.

Audience member: What kind of performance gains are you looking at?

Daniel: Compared to other browsers... It's lightning fast compared to IE; we'll see how it is with their new version. Chrome is extremely optimized; it closes the gap a good bit. The only statistic I know off the top of my head is that there was an announcement by the asm.js team that compared to running the code in a native C compiler, it's a slow-down factor of only 1.5, so it's close to native performance compared to anything else you can get on the web.

Audience member: I have a bunch of code for a DSP written in an SSC assembly. Is there a way I can transpile it into asm.js, or do I need to rewrite it in C++?

Daniel: I understand that Emscripten uses an intermediate language called LLVM and I didn't mention this before, but there is a separate tool that you must use to convert from C to LLVM and then to asm.js, so I think you're going to need some compilation tool that will target LLVM or transpile to C.

Audience member: Is asm converted to JVM bytecode, or is it converted to machine language.

Daniel: When you say JVM, do you mean Java Virtual Machine? (Yes?) it doesn't have anything to do with Java as far as I'm aware. As far as I understand, it is compiled to something like machine code, but I don't know the exact specifics. I would have to look into that.

Audience member: I believe that is a subset of the Javascript language that the interpreter of the browser can interpret like a JIT thing, but within Javascript into machine language that works with your cores, your GPU, you name it, and that's how it gets that speed bump?

Daniel: Right, and in addition to using asm.js, it also uses Web Workers to parallelize the computations for maximum efficiency in this particular application.

Audience member: You have to hand-code the Web Workers?

Daniel: Yes. Unfortunately, the tools do not generate the Web Workers for you and you're still stuck having to pass strings back and forth as messages, which is one of the intrinsic limitations of the Web Workers you may be aware of, but basically, you have a separate instance of asm.js running this code on each of your web workers.

Audience member: How does asm.js handle the implications of running native code in your web browser. Is there a sandbox for that?

Daniel: That's a good question and I guess I can speculate. As I pointed out, if you're using Emscripten, you can use that array to simulate, in the Javascript, allocated memory, but of course, it is treated as compiled code. I don't know what kind of vulnerabilities there might be in the transpiler that might somehow allow somebody to access something in the machine outside the browser, but as far as I'm aware, I think it should be sandboxed just like any other Javascript application, but you never know what vulnerabilities are beneath the surface.

::Camera cuts off:: Thank you.