CrazyPython
11/25/2018 - 7:29 PM

benchmark.md

Benchmark notes

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.