High-Performance WebGL Apps with LLJS and asm.js

James Long / @jlongster

CascadiaJS 2013

@jlongster

How possible?! wow

Machine Types


// signed 32-bit integers
(4.6 | 0) === 4
(2147483648 | 0) === -2147483648

// unsigned 32-bit integers
(-4 >>> 0) === 4294967292
          

Machine Types

32-bit integers all the way through


((4.6 | 0) + (5.7 | 0) | 0) === 9
          

It's fast because:

  • 100% type consistency
  • no garbage collection (manually managed memory in large typed array)

It's a good target for C/C++ apps, more realistic target than PNaCl in the long term since it can be used today

oh no, controversy

You might be interested in this for:

  • games
  • number crunching
  • video processing
  • anything CPU intense

yes, JavaScript is missing a few things

  • 64-bit integers
  • 32-bit floats
  • SIMD
  • pthreads

but a lot of it is coming (probably not pthreads though)

Let's give it a name

asm.js

asm.js-style code

  • hand-written
  • slightly verbose

for(var i = 1; i < 16; i++) {
    var br = 255 - ((func() * 96) | 0);
    var brr = (br + i) | 0;
}
         

asm.js spec

  • compiler generated
  • more explicit
  • still completely valid JS, no special forms

function asmModule(globals, env, heap) {
    "use asm";

    var F4 = new globals.Float32Array(heap);
    var random = env.random;

    function main(x) {
        var i = 0, br = 0, brr = 0;

        for(i = 1; (i | 0) < 16; i = (i | 0) + 1 | 0) {
            br = (255 - (((func() | 0) * 96) | 0)) | 0;
            brr = (br + i) | 0;
        }

        // usually reads/writes from types arrays (like F4)
        // a lot as well
    }

    return { main: main };
}
          

Ahead-of-time (AOT) compiling

  • relatively easy to integrate with existing JIT
  • highly optimized without much work
  • reliable performance
  • a lot of subtle benefits

Ahead-of-time (AOT) compiling

JIT compiling

  • ignores "use asm"
  • focuses on asm.js-style code
  • performance not as guaranteed

  • but everybody should do this anyway

https://bugzilla.mozilla.org/show_bug.cgi?id=860923

Current Benchmarks

native
firefox
chrome

Current Benchmarks

native
firefox
chrome

So how can I use it?

emscripten

https://github.com/kripken/emscripten

C/C++ -> LLVM -> JavaScript

  • preferred method
  • battle-hardened

Low-Level JavaScript (LLJS)

My fork: https://github.com/jlongster/LLJS

  • a fun experiment
  • adds bits of C to JavaScript
  • optional C-like types and manual memory management

pointers


let int x = 5;
let int *y = &x;

*y = 10;

// x equals 10
          

functions & stack allocated data


function void process(int* nums) {
    for(let int i=0; i<1000; i++) {
        nums[i] = i * 2;
    }
}

function void run() {
    // stack allocated
    let int nums[1000];
    nums[0] = 5;

    process(nums);
}
          

type casting & structs


struct vec2 {
    function void vec2(double x, double y) {
        this->x = x;
        this->y = y;
    }

    double x;
    double y;
}

function double length(vec2 *v1) {
    return sqrt(v1->x * v1->x + v1->y * v1->y);
}

function void run() {
    let vec2 point(100, 150);

    let double len = length(&point);
    let int rounded = int(len * 1000);
}
          
language still in flux, still researching

optional typing


struct vec2 {
    double x;
    double y;
}

let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');

function render() {
    let vec2 points[10000];
    // do some heavy processing on points

    ctx.fillStyle = 'red';

    for(let int i=0; i<10000; i++) {
        ctx.fillRect(points[i].x - 2,
                     points[i].y - 2,
                     4, 4);
    }
}
          

The ubiquitous vector


function foo() {
    var point = { x: 5, y: 10 };
    return point.x;
}
          

function foo() {
    var point = [5, 10];
    return point[0];
}
          

mat3.determinant = function (a) {
    var a00 = a[0], a01 = a[1], a02 = a[2],
        a10 = a[3], a11 = a[4], a12 = a[5],
        a20 = a[6], a21 = a[7], a22 = a[8];

    return a00 * (a22 * a11 - a12 * a21) + a01 * (-a22 * a10 + a12 * a20) + a02 * (a21 * a10 - a11 * a20);
};
          

LLJS vector


struct vec2 {
    double x;
    double y;
}

function double foo() {
    let vec2 point;
    point.x = 5;
    point.y = 10;

    return point.x;
}
          

look ma, no GC


function foo() {
  var _ = 0.0, $SP = 0;
  U4[1] = (U4[1] | 0) - 16 | 0;
  $SP = U4[1] | 0;

  F8[(($SP)) >> 3] = +5;
  F8[((($SP)) + 8 | 0) >> 3] = +10;

  return +(_ = +(+F8[(($SP)) >> 3]), U4[1] = (U4[1] | 0) + 16 | 0, _);
}
          


struct vec2 {
    double x;
    double y;
}

struct line {
    vec2 pos1;
    vec2 pos2;
}

function void foo() {
    let line AB;
    AB.pos1.x = 5;
    AB.pos1.y = 10;
    AB.pos2.x = 20;
    AB.pos2.y = 15;
}
          

pointers again


struct vec2 {
    double x;
    double y;
}

function update(vec2 *v) {
    // ...
}

function foo() {
    let vec2 pointA;
    let vec2 *pointB = &pointA;
    pointB->x = 5;

    let vec2 *pointC = new vec2();
}
          
   +---------------+
 0 | Heap  Pointer |
 1 | Stack Pointer |
   +---------------+ <- Heap Pointer (HP)
   |               |
   |               | |
   |     HEAP      | | Malloc Region
   |               | v
   |               |
   +---------------+
   |               |
   |               | ^
   |     STACK     | |
   |               | |
   |               |
   +---------------+ <- Stack Pointer (SP)
          

function foo() {
  var _ = 0.0, $SP = 0;
  U4[1] = (U4[1] | 0) - 16 | 0;
  $SP = U4[1] | 0;

  F8[(($SP)) >> 3] = +5;
  F8[((($SP)) + 8 | 0) >> 3] = +10;

  return +(_ = +(+F8[(($SP)) >> 3]), U4[1] = (U4[1] | 0) + 16 | 0, _);
}
          

compiler internals

  • 1. parse (esprima, patched with types)
  • 2. lift (estransform)
  • 3. resolve types & scan
  • 4. transform
  • 5. lower
  • 6. generate (escodegen)

estransform


BlockStatement.prototype.transform = function(o) {
    if(this.shouldTransform) {
        return new OtherTypeOfNode(this.argument);
    }
};

Identifier.prototype.transformNode = function(o) {
    this.internalName = '$' + this.name.value;
};
          
weren't we talking about asm.js?
asm.js-style code
asm.js code
  • pure LLJS: asm.js module (fully typed)
  • mixed LLJS: asm.js-style code mixed with js (partially typed)
things are buggy, especially mixed mode

What I did

  • forced type annotations on every expressions
  • wrapped code in asm.js module with "use asm"
  • fixed lots of various syntax forms to make it validate
  • rehauled some of internal representations of pointers

https://github.com/jlongster/LLJS

ljc code.ljs
ljc -a -m core -e init,update code.ljs

my custom fork, original repo is at mbebenita/LLJS

TODO

  • clean up the minefield of bugs
  • dynamic allocation (malloc/free)
  • sourcemaps
  • better integration between mixed and pure LLJS modules
  • maybe even rethink some language features

demo #1

code

demo #2

code

demo #3

code

thanks!

@jlongster