Search code examples
javascriptperformanceperformance-testingweb-worker

Why do my webworkers perform so unexpectedly bad?


In order to familiarize myself with web workers I have written a small test where 2 arrays are added element wise. The positions in the target array are distributed to 4 workers. I wanted to measure the performance and experienced a rude awakening.

///init///
const workers = new Array(4), global_elements = 250000;

function createArray(value, elements) {
    return new Int8Array(elements).fill(value);
}

let a = createArray(1, global_elements), b = createArray(2,global_elements), c = createArray(0,global_elements), data_recived = 0;

window.URL = window.URL || window.webkitURL;

for(let i=0; i<4; ++i) {
    let response = `self.onmessage=function(e){
                        for(let i=${i*global_elements}; i<${i*global_elements+global_elements}; ++i) {
                                e.data[2][i] = e.data[0][i] + e.data[1][i];
                        }
                        postMessage(0);
                    }`;
    workers[i] = new Worker(URL.createObjectURL(new Blob([response], {type: 'application/javascript'})));
    workers[i].onmessage = function(e) {
        if(++data_recived === 4) {
            t1 = performance.now();
            console.log(t1-t0);
        }
    };
}
///end-init///    

//normal
let t0 = performance.now();
for(let i=0; i<global_elements; ++i) {
    c[i] = a[i] + b[i];
}
let t1 = performance.now();
console.log(t1-t0);

//worker
t0 = performance.now();
for(let i=0; i<4; ++i) {
    workers[i].postMessage([a,b,c]);
}

Sadly, here workers can not even score with increasing global_elements number.

elements: normal | workers

2500: 0.1 | 51.4
25000: 1.5 | 66.5
250000: 4.1 | 182

(I know the performance tests are not optimal.)

Why do my webworkers perform so unexpectedly bad?


Solution

  • I've found that the way how to access the arrays in function has a big impact on performance. By saving them locally I was able to reduce calculation time by more then half.

    Task: 2DMatrixMul, elements: 250000, worker_count: 8
    normal needed: 1010 ms
    workers needed: 325 ms

    Here is what my little test case ended up with, for those who are interested.

    const worker_count = 8, workers = new Array(worker_count), global_elements = 250000;
    
    let data_recived = 0, a = new Tensor(1, [Math.sqrt(global_elements), Math.sqrt(global_elements)]), b = new Tensor(1, [Math.sqrt(global_elements), Math.sqrt(global_elements)]), c = new Tensor(0, [Math.sqrt(global_elements), Math.sqrt(global_elements)]);
    
    window.URL = window.URL || window.webkitURL;
    
    for(let i=0; i<worker_count; ++i) {
        let response = `
                        self.onmessage=function(e){
                            const a = e.data[0], b = e.data[1], tmp = new Float32Array(${global_elements/worker_count});
                            let c=${i*global_elements/worker_count}, i=0, z=0, m=0, lim = 0;
                            for(; c<${i*global_elements/worker_count+global_elements/worker_count}; ++c) {
                                i = Math.floor(c/${c._shape._dim[0]}), z = c%${c._shape._dim[0]};
                                for(m=i*${a._shape._dim[1]}, lim = m+${a._shape._dim[1]}; m<lim; ++m) {
                                        tmp[c-${i*global_elements/worker_count}] += a[m] * b[(m - i*${a._shape._dim[1]})*${b._shape._dim[1]}+z];
                                }
                            }
                            postMessage(tmp);
                        }`;
        workers[i] = new Worker(URL.createObjectURL(new Blob([response], {type: 'application/javascript'})));
        workers[i].onmessage = function(e) {
            pass(c._value, e.data, i);
            if(++data_recived === worker_count) {
                t1 = performance.now();
                console.log(t1-t0);
                console.log(c);
            }
        };
    }
    
    function pass(arr, data, index) {
        for(let i=index*global_elements/worker_count, len=index*global_elements/worker_count+global_elements/worker_count; i<len; ++i) {
            arr[i] = data[i-index*global_elements/worker_count];
        }
    }
    ///end-init///    
    
    //normal
    let t0 = performance.now();
    calculations.matmul2D(a,b);
    let t1 = performance.now();
    console.log(t1-t0);
    
    //worker
    t0 = performance.now();
    for(let i=0; i<worker_count; ++i) {
        workers[i].postMessage([a._value,b._value]);
    }
    
    /* calculations.matmul2D( ) looks like this:
    
    static matmul2D(a, b) { //revisite
        const row = a._shape._dim[0], column = b._shape._dim[1], a1 = a._shape._dim[1];
        let c=0, i=0, m=0, lim=0, len=row * column, result = new Tensor(0, [a._shape._dim[0], column]);
        for(; c<len; ++c) {
            i = Math.floor(c/row);
            for(m=i*a1, lim = m+a1; m<lim; ++m) {
                result._value[c] += a._value[m] * b._value[(m - i*a1)*column+(c%row)];
            }
        }
        return result;
    }
    
    */