Putting the code within for (let j = 0; j < 100; j++)
in its own function did not affect results in either benchmark.
Removing this.x = 0; this.y = 0;
did not affect results for the JS Array benchmark.
Passing --trace-opt
and --trace-deopt
shows that the JIT optimizes all lines of code for both benchmarks in the first few iterations; it does not repeatedly try to re-optimize for either.
The results scale. When passing j < 300
, the JS Array takes 15-17 seconds while the Int8Array takes 0.54 seconds. That's a 27x performance improvement for a pure read/write/multiply ALU test.
More complex loop benchmark (if/else branch) Surprisingly, the Int8Array (now turned into a Float64Array) isn't just faster at loads and stores. Here's a floating point arithmetic benchmark (the code that's identical to the previous benchmark have been omitted):
function bench() {
let buf = []
for (let i = 0; i < 100000; ++i) {
buf[i] = new obj();
buf[i].x = Math.random()*2;
buf[i].y = Math.random()*3;
}
for (let i = 0; i < 100000; ++i) {
buf[i].x += Math.random();
buf[i].y += Math.random();
}
for (let i = 0; i < 100000; ++i) {
buf[i].x *= Math.random();
buf[i].y *= Math.random();
}
}
node -e 6.19s user 0.58s system 194% cpu 3.475 total
function bench() {
buf = new Float64Array(100000)
for (let i = 0; i < 100000; ++i) {
(new obj(i)).x = Math.random()*2;
(new obj(i)).y = Math.random()*3;
}
for (let i = 0; i < 100000; ++i) {
(new obj(i)).x += Math.random();
(new obj(i)).y += Math.random();
}
for (let i = 0; i < 100000; ++i) {
(new obj(i)).x *= Math.random();
(new obj(i)).y *= Math.random();
}
}
node -e 1.24s user 0.06s system 98% cpu 1.247 total
We can even throw in an unpredictable branch prediction and the affect remains:
function bench() {
let buf = []
for (let i = 0; i < 100000; ++i) {
buf[i] = new obj();
buf[i].x = Math.random()*2;
buf[i].y = Math.random()*3;
}
for (let i = 0; i < 100000; ++i) {
if(Math.random() > 0.5) {
buf[i].x += Math.random();
} else if (i > 0) {
buf[i].x -= (buf[i-1].y)*Math.random();
}
buf[i].x += Math.random();
buf[i].y += Math.random();
}
for (let i = 0; i < 100000; ++i) {
buf[i].x *= Math.random();
buf[i].y *= Math.random();
}
}
node -e 6.64s user 0.56s system 173% cpu 4.149 total
function bench() {
buf = new Float64Array(100000)
for (let i = 0; i < 100000; ++i) {
(new obj(i)).x = Math.random()*2;
(new obj(i)).y = Math.random()*3;
}
for (let i = 0; i < 100000; ++i) {
if(Math.random() > 0.5) {
(new obj(i)).x += Math.random();
} else if (i > 0) {
(new obj(i)).x -= (new obj(i-1)).y*Math.random();
}
(new obj(i)).y += Math.random();
}
for (let i = 0; i < 100000; ++i) {
(new obj(i)).x *= Math.random();
(new obj(i)).y *= Math.random();
}
}
node -e 1.58s user 0.12s system 93% cpu 1.813 total
Combining the three loops into one made the JS Array version ~0.2 seconds faster and did not observably affect the speed of the Int8Array. Combining the loops always made it 0.2 seconds faster, even when j < 300
was used.
Adding a new z property (below) results in 0.8 additional seconds for 2.0 seconds. Still faster than the JS Array.
// class obj {
get x() { return buf[this.index*3] }
set x(v) { buf[this.index*3] = v }
get y() { return buf[this.index*3 + 1] }
set y(v) { buf[this.index*3 + 1] = v }
get z() { return buf[this.index*3 + 2] }
set z(v) { buf[this.index*3 + 2] = v }
// first loop
(new obj(i)).z = Math.random()*2.78243;
// second loop
(new obj(i)).z += Math.random();
// third loop
(new obj(i)).z *= Math.random();
Just for fun, let's throw in a string test in there.
~ time node -e "
let log
class obj {
constructor() {this.x=0; this.y=0}
hello() {
log += 'hi' + this.x + this.y
}
}
function bench() {
let buf = []; log = ''
for (let i = 0; i < 100000; ++i) {
buf[i] = new obj();
buf[i].x = Math.random()*2;
buf[i].y = Math.random()*3;
}
for (let i = 0; i < 100000; ++i) {
if(Math.random() > 0.5) {
buf[i].x += Math.random();
} else if (i > 0) {
buf[i].x -= (buf[i-1].y)*Math.random();
}
buf[i].x += Math.random();
buf[i].y += Math.random();
}
for (let i = 0; i < 100000; ++i) {
buf[i].x *= Math.random();
buf[i].y *= Math.random();
buf[i].hello()
}
}
for(let i = 0; i < 80; ++i) {
bench()
}"
node -e 19.19s user 1.21s system 152% cpu 13.408 total
~ time node -e "let buf
let log = ''
class obj {
constructor(index) {
this.index = index
}
get x() {
return buf[this.index * 2]
}
set x(v) {
buf[this.index * 2] = v
}
get y() {
return buf[this.index * 2 + 1]
}
set y(v) {
buf[this.index *2 + 1] = v
}
hello() {
log += 'hi' + this.x + this.y
}
}
function bench() {
buf = new Float64Array(100000)
log = ''
for (let i = 0; i < 100000; ++i) {
(new obj(i)).x = Math.random()*2;
(new obj(i)).y = Math.random()*3;
}
for (let i = 0; i < 100000; ++i) {
if(Math.random() > 0.5) {
(new obj(i)).x += Math.random();
} else if (i > 0) {
(new obj(i)).x -= (new obj(i-1)).y*Math.random();
}
(new obj(i)).y += Math.random();
}
for (let i = 0; i < 100000; ++i) {
(new obj(i)).x *= Math.random();
(new obj(i)).y *= Math.random();
(new obj(i)).hello();
}
}
for (let j = 0; j < 80; j++) {
bench()
};"
node -e 7.80s user 0.52s system 121% cpu 6.857 total
Simply put, using an ArrayBuffer is just faster.