art with code

2010-02-11

4x4 float matrix multiplication using SSE intrinsics

Here's a bit of fun from earlier this week: 4x4 matrix multiplication using SSE. Benchmarked it to be 4x faster than the scalar version (on a Pentium M, using GCC 4.4.1 with flags -O3 -mfpmath=sse -msse2 -std=c99).

To refresh your memory, here's the definition of matrix multiplication: A x B = R, where R[i][j] = dot(row(A, i), column(B, j)) and dot(u, v) = sum(i=0 to N, u[i] * v[i]). Only defined when A's width equals B's height. Matrix multiplication is used for transformations in computer graphics, 4x4 matrix multiplication specifically in 3D graphics.

First the scalar version. Multiply A with B and store the result in R. The matrices are stored in column-major order in a flat float array (so that the 4x4 transform matrix translation column is the last four values in the array).

void
mmul(const float *a, const float *b, float *r)
{
for (int i=0; i<16; i+=4)
for (int j=0; j<4; j++)
r[i+j] = b[i]*a[j] + b[i+1]*a[j+4] + b[i+2]*a[j+8] + b[i+3]*a[j+12];
}


Now, a naive slow SSE version. Load a row from A and a column from B, multiply them together and add the values of the result vector together.

Note that we need to align all the matrices to 16-byte boundaries. You can use memalign(16, 16*sizeof(float)) or posix_memalign or compiler pragmas to do that. If the matrices are not aligned to 16-byte boundaries, the SSE code will segfault. I'm using the SSE intrinsics, so you won't see any cool inline assembler tricks here.

void
mmul_sse_dot(const float * a, const float * b, float * r)
{
__m128 a_line, b_line, r_line;
float mc[16] __attribute__((aligned(16))); // 16-byte aligned temp array
for (int i=0; i<16; i+=4) {
b_line = _mm_load_ps(&b[i]); // b_line = vec4(column(b, i))
// remember that we are column-major
for (int j=0; j<4; j++) {
a_line = _mm_set_ps(a[j+12], a[j+8], a[j+4], a[j]);
// a_line = vec4(row(a, j))
// note that SSE is little-endian
r_line = _mm_mul_ps(a_line, b_line); // r_line = a_line * b_line
_mm_store_ps(mc, r_line); // copy r_line to memory
r[i+j] = mc[0]+mc[1]+mc[2]+mc[3]; // r[i][j] = sum(r_line)
// = dot(a_line, b_line)
}
}
}

Sadly, this way is slow in practice (30% slower than the scalar version, in fact) as it stores only one value at a time and there's no operation to sum the elements of an SSE vector.

What we must do instead is execute four dots in parallel. Multiply the first value of the row A[I] with the first row of B and store the result to row R[I] (R[I] = A[I][0] * B[0]). Then multiply the second value of row A[I] with the second row of B, add to row R[I] (R[I] += A[I][1] * B[1]). Repeat for all the remaining values in row A[I].

Now the first value of row R[I] is the sum of products of the row A[I] and the first values of the rows of B. I.e. the dot product of row A[I] and the first column of B.

Correspondingly, the second value of R[I] is the sum of products of row A[I] and the second values of the rows of B. I.e. the dot of row A[I] and the second column of B. And so forth.

In pseudo-code:

R[I] = A[I][0] * B[0] +
A[I][1] * B[1] +
A[I][2] * B[2] +
A[I][3] * B[3]

But as our matrices are column-major, we need to flip A and B, so that we're multiplying whole columns with the values of a row. Ok, time for code:

void
mmul_sse(const float * a, const float * b, float * r)
{
__m128 a_line, b_line, r_line;
for (int i=0; i<16; i+=4) {
// unroll the first step of the loop to avoid having to initialize r_line to zero
a_line = _mm_load_ps(a); // a_line = vec4(column(a, 0))
b_line = _mm_set1_ps(b[i]); // b_line = vec4(b[i][0])
r_line = _mm_mul_ps(a_line, b_line); // r_line = a_line * b_line
for (int j=1; j<4; j++) {
a_line = _mm_load_ps(&a[j*4]); // a_line = vec4(column(a, j))
b_line = _mm_set1_ps(b[i+j]); // b_line = vec4(b[i][j])
// r_line += a_line * b_line
r_line = _mm_add_ps(_mm_mul_ps(a_line, b_line), r_line);
}
_mm_store_ps(&r[i], r_line); // r[i] = r_line
}
}

Now we're cooking. 4x faster than the scalar version. And it even produces the same results.

You can get the full code here: mmul.c.

[Edit]
Wrote a C++ version with a vec4 struct to make the code super short and simple:

void mmul_vec4
(const float * a, const float * b, float * r)
{
for (int i=0; i<16; i+=4) {
vec4 rl = vec4(a) * vec4(b[i]);
for (int j=1; j<4; j++)
rl += vec4(&a[j*4]) * vec4(b[i+j]);
rl >> &r[i];
}
}

The best thing? Vec4 is actually a wrapper on __m128 SSE operations, so mmul_vec4 runs as fast as mmul_sse. And the wrapper was dirt simple to write too, see below.

struct vec4
{
__m128 xmm;

vec4 (__m128 v) : xmm (v) {}

vec4 (float v) { xmm = _mm_set1_ps(v); }

vec4 (float x, float y, float z, float w)
{ xmm = _mm_set_ps(w,z,y,x); }

vec4 (const float *v) { xmm = _mm_load_ps(v); }

vec4 operator* (const vec4 &v) const
{ return vec4(_mm_mul_ps(xmm, v.xmm)); }

vec4 operator+ (const vec4 &v) const
{ return vec4(_mm_add_ps(xmm, v.xmm)); }

vec4 operator- (const vec4 &v) const
{ return vec4(_mm_sub_ps(xmm, v.xmm)); }

vec4 operator/ (const vec4 &v) const
{ return vec4(_mm_div_ps(xmm, v.xmm)); }

void operator*= (const vec4 &v)
{ xmm = _mm_mul_ps(xmm, v.xmm); }

void operator+= (const vec4 &v)
{ xmm = _mm_add_ps(xmm, v.xmm); }

void operator-= (const vec4 &v)
{ xmm = _mm_sub_ps(xmm, v.xmm); }

void operator/= (const vec4 &v)
{ xmm = _mm_div_ps(xmm, v.xmm); }

void operator>> (float *v)
{ _mm_store_ps(v, xmm); }

};

Here's the full code for the C++ version: mmul.cpp.

2009-12-17

3D models and parsing binary data with JavaScript

Background


A few days ago, I was fiddling around with loading 3D models for use with WebGL (here demos). I had an OBJ parser and some test models lying around from an earlier project, and the parser was only a two hundred lines, so I ported it over to JavaScript. But the OBJ files were pretty huge, a 40-thousand quad model without normals or texcoords was 2.4 megs. Not really something you want to send over the internet.

The OBJ file format stores coordinates and face indexes as text. While it makes the model files human-readable and easy to parse, it also increases the file size. Uncompressed, a single coord takes 9 bytes, plus an optional sign byte.

My first instinct was to just use gzip compression on server-side. And it did manage to bring the file size down to 880k for the above-mentioned 2.4 meg OBJ. Still. Maybe there would be more gains to be had by storing the coords and face indexes as binary data.

I went for [0,1]-normalized 16-bit fixed-point numbers for the coordinates and 16-bit unsigned ints for the face indexes. I also had to store the vertex and face counts, for which I used 32-bit unsigned ints (which, come think of it, is a bit silly, as using 16-bit face indexes limits the vert count to 65536). Finally, I added two three-float vectors for coord scaling and translation, in order to inflate the normalized coords back to something close to their original values.

Result: 560k for uncompressed binary file, 470k gzipped. Nice. Though it could be brought down further by using a triangle strip instead of separate quads and tris. And there are fancy 3D mesh compression papers that talk about compressing down to less than 2 bits per triangle (for the example model: 40kquads -> 80ktris = 160kb = 20kB). But enough of that for now. Next up, how to parse the binary data.

Parsing binary data


To parse a binary string with JavaScript, you use charCodeAt and the usual bit munging operators (&, |, <<, >>). First, you need the byte value at some point in the string. As charCodeAt operates on characters and not bytes, you need to tell the browser to get the data as an unparsed string by calling overrideMimeType("text/plain; charset=x-user-defined") on your XMLHttpRequest. Then mask away the high byte of each char code to get the low byte value. The following works at least on Firefox and WebKit, I don't know about IE and Opera.

var byteValue = str.charCodeAt(index) & 0xFF;

Loading unsigned ints is simple enough, just load the bytes for the int and shift them to their places:

// read big-endian (network byte order) unsigned 32-bit int from data, at offset
readUInt32 = function(data, offset) {
return ((data.charCodeAt(offset) & 0xFF) << 24) +
((data.charCodeAt(offset+1) & 0xFF) << 16) +
((data.charCodeAt(offset+2) & 0xFF) << 8) +
(data.charCodeAt(offset+3) & 0xFF);
}
// read big-endian (network byte order) unsigned 16-bit int from data, at offset
readUInt16 = function(data, offset) {
return ((data.charCodeAt(offset) & 0xFF) << 8) +
(data.charCodeAt(offset+1) & 0xFF);
}

Reading in normalized fixed-point numbers as floats isn't much harder either, as they're generated with uint16(normalized_float * 65535):

readNormalizedUFixedPoint16 = function(data, offset) {
return readUInt16(data, offset) / 65535.0;
}

Floats, on the other hand, are a bit more involved. Writing the float parser was educational, now I have a better understanding of the buggers! The following code doesn't handle the special numbers and likely loses precision, is slow, etc., so if you have a better way of parsing binary floats, please share.

// read big-endian (network byte order) 32-bit float
readFloat32 = function(data, offset) {
var b1 = data.charCodeAt(offset) & 0xFF,
b2 = data.charCodeAt(offset+1) & 0xFF,
b3 = data.charCodeAt(offset+2) & 0xFF,
b4 = data.charCodeAt(offset+3) & 0xFF;
var sign = 1 - (2*(b1 >> 7)); // sign = bit 0
var exp = (((b1 << 1) & 0xff) | (b2 >> 7)) - 127; // exponent = bits 1..8
var sig = ((b2 & 0x7f) << 16) | (b3 << 8) | b4; // significand = bits 9..31
if (sig == 0 && exp == -127)
return 0.0;
return sign * (1 + sig * Math.pow(2, -23)) * Math.pow(2, exp);
}

And there you have it. I haven't gotten around to parsing signed integers, though I guess subtracting 1 << (bits-1) from the unsigned value would do the trick.

2009-11-28

JavaScript drawing apps

Wrote two small drawing apps during the last two days:

WebGL cube paint - draw with fancy 3D cubes.




MiniPaint - a 2D canvas drawing app in 25 lines.



The 25-line app has Wacom support (pressure, device type, tilt). Too bad that the only copy of Firefox with Wacom support is mine. (Yes, created bugs in Bugzilla and sent patches. Wait and see. BTW, if you know how to add Wacom support for Windows and Mac, that'd be awesome.)

2009-11-23

WebGL cubes

Started writing a small scene graph last weekend. Here cube shots from test demo:




I wrote and optimized one use-case for it as well! "Create bouncing cube on canvas (with lighting etc.)"

Ended up with the special-case-rife DWIM snippet: new Scene($('gl_canvas'), new Cube().makeBounce()).

Demos: lots of cubes and a simple bouncing cube.

2009-11-16

Further optimizing a small JavaScript loop

Continuing from Optimizing a small JavaScript loop, there were some suggestions in the comments on how it might be made faster (or cleaner.) I tested them all just now, along with a couple ideas of my own.

Using var l = imgs.length; while (l--) instead of a for-loop


No measurable effect on the speed of my loop, except that I had to reverse a sort order to account for the reverse traversal. And I don't like reversing sort orders, it's hard enough to keep these things straight when you're not counting backwards.

Not copying function args to local vars


I.e. instead of the redundant var ctop = arg2, use arg2 directly. Made the code cleaner. Didn't have any measurable effect on runtime.

Using object properties directly instead of copying them to local vars


I.e. instead of var etop = e.cachedTop use e.cachedTop directly. Made the code cleaner, no effect on runtime.

Using imgs[i] everywhere instead of var e = imgs[i]


That seems slower to me. And more typing. But, no measurable effect on runtime.

Using a > b ? a : b instead of Math.max(a,b), ditto for Math.min


This was one of my ideas. No measurable effect on runtime.

Inlining isVisible to the loop


Again, one of my ideas. Again, no measurable effect on runtime. Maybe a 5-10% improvement, but my measurement error is 10-20% :|

Moving setting e.trans outside the loop


Sounds like a good idea. Need to refactor my code to do that.

Conclusions


Most of the things people tell you will improve JavaScript performance actually don't. Measure.

Also, I need a better way to measure runtimes. What I have now is a keydown handler that runs the loop a hundred times and prints the elapsed time to Firebug console. The amusing thing is that the exact same code can give 100-run runtimes ranging from 550 ms to 720 ms. And my system load is zero.

I suppose the problem is that this benchmark is run as part of my existing application. So, if any enterprising soul wants to have a go, make the bit you benchmark a separate page.

2009-11-14

Optimizing a small JavaScript loop

I recently had to optimize a visibility-checking loop, as it was causing some slowness when dealing with several hundreds of images. Executive summary: profile with Firebug, cache regex results and unchanging DOM properties.

I started with a loop like this:

imgs.forEach(function(e) {
if (isVisible(e)) {
if (e.src.match(/\/transparent\.gif$/)) {
e.src = e.getAttribute('origsrc');
}
} else if (!e.src.match(/\/transparent\.gif$/)) {
e.src = '/transparent.gif';
}
});

isVisible = function(e) {
var ctop = window.scrollY;
var cbottom = ctop + window.innerHeight;
var etop = elem.offsetTop;
var ebottom = etop + elem.offsetHeight;
var top = Math.max(ctop, etop);
var bottom = Math.min(cbottom, ebottom);
return (bottom - top) > -1200;
}

Runtime for 400 images: 35ms. Gah!

Profile says that a lot of time is spent in isVisible. But there's really nothing there that looks expensive. Let's try caching window.scrollY and window.innerHeight anyhow.

var wsy = window.scrollY;
var wih = window.innerHeight;

imgs.forEach(function(e) {
if (isVisible(e, wsy, wih)) {
...

isVisible = function(e, wsy, wih) {
var ctop = wsy;
var cbottom = ctop + wih;
...

Runtime came down to 23 ms. Whoah, that's a third off the execution time. Maybe property access is super expensive, let's see if caching elem.offsetTop and elem.offsetHeight would help as well.

isVisible = function(e, wsy, wih) {
...
if (e.cachedTop == null || !e.complete) {
e.cachedTop = e.offsetTop;
e.cachedBottom = e.cachedTop + e.offsetHeight;
}
var etop = e.cachedTop
var ebottom = e.cachedBottom
...
}

Runtime down to 15 ms. Excellent.

Profile says that the forEach loop is using a lot of time. Maybe it's the regexes. How about using RegExp.test instead of match. The docs say it should be faster.

if ((/\/transparent\.gif$/).test(e.src)) {
...
} else if (!(/\/transparent\.gif$/).test(e.src)) {


Runtime down to 12 ms. RegExp.test is indeed faster than String.match. Next, we could do away with the regexp altogether as we control which images are transparent and which are not...


imgs.forEach(function(e) {
if (e.trans == null)
e.trans = (/\/transparent\.gif$/).test(e.src);
if (isVisible(e, wsy, wih)) {
if (e.trans) {
e.src = e.getAttribute('origsrc');
e.trans = false;
e.cachedTop = e.cachedBottom = null;
}
} else if (!e.trans) {
e.src = '/transparent.gif';
e.trans = true;
e.cachedTop = e.cachedBottom = null;
}
});

Okay, now we're down to 5 ms. Finally, let's try replacing the forEach-loop with a plain for-loop.

for (var i=0; i<imgs.length; i++) {
var e = imgs[i];
...
}

Down to 3 ms. Not all that much in absolute terms, but relative to 5 ms it's 40% less. I wonder if CRAZY MICRO-OPTIMIZATIONS would do anything.

for (var i=0, l=imgs.length, e=imgs[0]; i<l; e=imgs[++i]) {
...
}

Down to ... 3 ms. No change. Well, it might be 0.3 ms faster but my measurements have a larger error than that. Ditch the micro-optimizations for readability.

And 'lo! The final code that is 10x faster than what we started with:

var wsy = window.scrollY;
var wih = window.innerHeight;

for (var i=0; i<imgs.length; i++) {
var e = imgs[i];
if (e.trans == null)
e.trans = (/\/transparent\.gif$/).test(e.src);
if (isVisible(e, wsy, wih)) {
if (e.trans) {
e.src = e.getAttribute('origsrc');
e.trans = false;
e.cachedTop = e.cachedBottom = null;
}
} else if (!e.trans) {
e.src = '/transparent.gif';
e.trans = true;
e.cachedTop = e.cachedBottom = null;
}
}

isVisible = function(e, wsy, wih) {
var ctop = wsy;
var cbottom = ctop + wih;
if (e.cachedTop == null || !e.complete) {
e.cachedTop = e.offsetTop;
e.cachedBottom = e.cachedTop + e.offsetHeight;
}
var etop = e.cachedTop
var ebottom = e.cachedBottom
var top = Math.max(ctop, etop);
var bottom = Math.min(cbottom, ebottom);
return (bottom - top) > -1200;
}

Does it work right? It seems to, at least. So maybe! Or maybe not! Isn't caching fun?

[Edit] The offsetTop and offsetHeight caching didn't work on my case, so I had to remove them. Final runtime 11 ms, or only 3x faster. Maybe some day I figure out why it didn't work...

[Edit] People have suggested using var l = imgs.length; while (l--) { ... }, which not only looks odd and reverses the iteration order, but also generates the same code as a for-loop would (on WebKit at least), according to Oliver Hunt. Well, the codegen test is for var i=0; while (i < imgs.length) i++; but i assume it also applies for for (var l=imgs.length; l--;).

[Edit] It's also somewhat strange to see suggestions that focus on optimizing all these weird things like "use object properties directly instead of local variables", instead of the possibly big things: inline isVisible, replace Math.max and Math.min with a > b ? a : b and a < b ? a : b. Well. I'll try them all and post a follow-up. Stay tuned!

[Edit] Follow-up.

2009-11-11

Canvas-generated hexa background


Wrote that for the new fancy Metatunnel page (while procrastinating on writing an essay...) Here's the JS to boggle over:

function polygon(n) {
var a = [];
for (var i=0; i<n; i++)
a.push({x:Math.cos(2*Math.PI*(i/n)), y:Math.sin(2*Math.PI*(i/n))});
return a;
}
function id(i){ return i }
function lineTo(ctx){return function (p){ ctx.lineTo(p.x, p.y) }}
function linePath(ctx, path){ path.map(lineTo(ctx)) }
function drawPath(ctx, path, close){
ctx.beginPath(); linePath(ctx, path); if (close) ctx.closePath() }
function strokePath(ctx, path, close){ drawPath(ctx, path, true); ctx.stroke() }
function fillPath(ctx, path){ drawPath(ctx, path); ctx.fill() }
function scale(x,y,p){return {x:p.x*x, y:p.y*y}}
function scalePoint(x,y){return function(p){ return scale(x,y,p) }}
function translate(x,y,p){return {x:p.x+x, y:p.y+y}}
function translatePoint(x,y){return function(p){ return translate(x,y,p) }}
function scalePath(x,y,path){ return path.map(scalePoint(x,y)) }
function translatePath(x,y,path){ return path.map(translatePoint(x,y)) }
function clonePath(path){ return path.map(id) }

function wavePoint(p) {
var np = {
x: (p.x + 1.5*Math.cos((p.x+p.y) / 15)),
y: (p.y + 1.5*Math.sin((p.x+p.y) / 15))
}
var d = Math.sqrt(np.x*np.x + np.y*np.y);
var s = Math.pow(1/(d+1),0.7);
return translate(5, 24, scale(s*30, s*30, np));
}
function wavePath(path){ return path.map(wavePoint) }
function drawbg() {
var canvas = document.createElement('canvas');
canvas.width = 1400;
canvas.height = 700;
var ctx = canvas.getContext('2d');
ctx.fillStyle = '#333';
ctx.fillRect(0,0,canvas.width,canvas.height);
var g = ctx.createRadialGradient(50,50,0, 50,50,1400);
g.addColorStop(0,'rgba(0,192,255,1)');
g.addColorStop(1.0,'rgba(255,0,255,1)');
ctx.fillStyle = g;
var hex = polygon(6);
var s = Math.sin(Math.PI/3);
var c = Math.cos(Math.PI/3);
for (var i=-2; i<120; i++) {
for (var j=0; j<8; j++) {
if (Math.random()+Math.random() < Math.pow((Math.sqrt(i*i+j*j) / 55),2)) continue;
var x = i*(1.2+c);
var y = j*(2.4*s) + (i%2 ? 1.2*s : 0);
fillPath(ctx, scalePath(10, 7, wavePath(translatePath(x,y,hex))));
}
}
document.body.style.background = '#333 url('+canvas.toDataURL()+') no-repeat top left';
}

Blog Archive