Сделайте потоки Javascript быстрыми

Недавно я пытался использовать интерфейс Web Workers для экспериментов с потоками в JavaScript.

Попытка создать contains с помощью веб-воркеров, выполнив следующие действия:

  • Разбить исходный массив на части одинакового размера
  • Создайте веб-воркер для каждой части, которая запускает .contains на этой части.
  • Когда и если значение найдено в любой из частей, он возвращает true, не дожидаясь завершения всех рабочих процессов.

Вот что я пробовал:

var MAX_VALUE = 100000000;
var integerArray = Array.from({length: 40000000}, () => Math.floor(Math.random() * MAX_VALUE));

var t0 = performance.now();
console.log(integerArray.includes(1));
var t1 = performance.now();
console.log("Call to doSomething took " + (t1 - t0) + " milliseconds.");

var promises = [];
var chunks = [];
while(integerArray.length) {
    chunks.push(integerArray.splice(0,10000000));
}

t0 = performance.now();
chunks.forEach(function(element) {
    promises.push(createWorker(element));
});

function createWorker(arrayChunk) {
    return new Promise(function(resolve) {
        var v = new Worker(getScriptPath(function(){
            self.addEventListener('message', function(e) {
                var value = e.data.includes(1);
                self.postMessage(value);
            }, false);
        }));
        v.postMessage(arrayChunk);
        v.onmessage = function(event){
            resolve(event.data);
        };
    });
}

firstTrue(promises).then(function(data) {
    // `data` has the results, compute the final solution
    var t1 = performance.now();
    console.log("Call to doSomething took " + (t1 - t0) + " milliseconds.");
});

function firstTrue(promises) {
    const newPromises = promises.map(p => new Promise(
        (resolve, reject) => p.then(v => v && resolve(true), reject)
));
   newPromises.push(Promise.all(promises).then(() => false));
    return Promise.race(newPromises);
}

//As a worker normally take another JavaScript file to execute we convert the function in an URL: http://stackoverflow.com/a/16799132/2576706
function getScriptPath(foo){ return window.URL.createObjectURL(new Blob([foo.toString().match(/^\s*function\s*\(\s*\)\s*\{(([\s\S](?!\}$))*[\s\S])/)[1]],{type:'text/javascript'})); }

Любой браузер и процессор пробовали, это очень медленно по сравнению с простым contains для исходного массива.

Почему это так медленно? Что не так с кодом выше?

использованная литература

Изменить: проблема не в .contains() конкретно, а в других функциях массива, например. .indexOf(), .map(), forEach() и т. д. Почему разделение работы между веб-воркерами занимает гораздо больше времени...


person Jannes Botis    schedule 21.12.2018    source источник
comment
Я собираюсь провести дополнительное исследование и опубликовать лучший комментарий, но мне интересно, может ли это быть вызвано низкой производительностью собственного метода includes.   -  person Pytth    schedule 21.12.2018


Ответы (2)


Это немного надуманный пример, поэтому трудно помочь оптимизировать то, что вы пытаетесь сделать конкретно, но одним из легко упускаемых из виду и исправимым медленным путем является копирование данных в веб-воркер. Если возможно, вы можете использовать ArrayBuffers и SharedArrayBuffers для быстрой передачи данных в веб-воркеры и обратно.

Вы можете использовать второй аргумент функции postMessage, чтобы передать право собственности на arrayBuffer веб-воркеру. Важно отметить, что этот буфер больше не будет использоваться основным потоком, пока он не будет передан обратно веб-воркером. SharedArrayBuffers не имеют этого ограничения и могут быть прочитаны многими рабочими одновременно, но не обязательно поддерживаются во всех браузерах из соображений безопасности (см. mdn для более подробной информации)

Например

const arr = new Float64Array(new ArrayBuffer(40000000 * 8));

console.time('posting');
ww.postMessage(arr, [ arr.buffer ]);
console.timeEnd('posting');

занимает ~ 0,1 мс, пока

const arr = new Array(40000000).fill(0);

console.time('posting');
ww.postMessage(arr, [ arr ]);
console.timeEnd('posting');

для запуска требуется ~ 10000 мс. Это ТОЛЬКО для передачи данных в сообщении, а не для запуска самой рабочей логики.

Вы можете прочитать больше об аргументе postMessage transferList здесь и передаваемые типы здесь. Важно отметить, что способ, которым ваш пример выполняет сравнение времени, включает также время создания веб-работника, но, надеюсь, это дает лучшее представление о том, куда уходит большая часть этого времени и как его можно лучше обойти.

person Garrett Johnson    schedule 28.12.2018

Вы делаете гораздо больше работы между t0 и t1 по сравнению с простым contains. Эти дополнительные шаги включают в себя:

  1. функция преобразования -> строка -> регулярное выражение -> большой двоичный объект -> URL-адрес объекта
  2. вызов нового работника -> анализирует URL-адрес объекта -> движок JS интерпретирует код
  3. отправка веб-данных -> сериализация в основном потоке -> десериализация в рабочем потоке (вероятно, в структуре памяти, которая фактически скопирована, поэтому не очень медленная)

Лучше сначала создать поток, а затем постоянно передавать ему данные. Это может быть не быстрее, но это не заблокирует ваш пользовательский интерфейс. Кроме того, если вы постоянно просматриваете массив, могу ли я предложить преобразовать его в карту, где ключом является значение массива, а значением является индекс.

например массив ['apple', 'coconut', 'kiwi'] будет преобразован в { apple: 1, coconut: 2, kiwi:3 } поиск по карте будет происходить в амортизированное нормальное время (быстро), а массив будет линейным поиском (чертовски медленный для больших наборов).

person Hedzer    schedule 21.12.2018
comment
хм... возможно, вы правы... Я пытался изменить promises.push(createWorker(element)); в promises.push(createWorker()); и значение переменной = e.data.includes(1); значение переменной = ложь; и это намного быстрее... Кажется, что копирование данных в поток - это проблема... Не могли бы вы предоставить какой-нибудь код, как передать данные позже, или я могу поделиться одним и тем же массивом, не копируя его? - person Jannes Botis; 22.12.2018