M. Heus
M. Heus

Reputation: 81

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?

Upvotes: 1

Views: 353

Answers (2)

M. Heus
M. Heus

Reputation: 81

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;
}

*/

Upvotes: 2

Nicholas Tower
Nicholas Tower

Reputation: 85012

I suspect the cause of the performance problem is the copying which occurs when sending data to a worker. Memory is not shared between the main thread and the worker, so passing messages usually involves cloning the data you want to send. See more info here: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#Transferring_data_to_and_from_workers_further_details

Some browsers support a technique called Transferable Objects, which lets you pass an object without cloning it. You can read more about that here: https://developers.google.com/web/updates/2011/12/Transferable-Objects-Lightning-Fast

Upvotes: 1

Related Questions