使 Javascript 个线程变快
Make Javascript Threads Fast
最近我一直在尝试使用Web workers接口在JavaScript.
中试验线程
尝试使用网络工作者 contains,执行以下步骤:
- 将初始数组拆分为大小相等的部分
- 为每个运行 .contains 的片段创建一个网络工作者
- 如果在任何片段中找到值,则 returns 为真,无需等待所有 worker 完成。
这是我尝试过的:
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:
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'})); }
任何浏览器和 cpu 都尝试过,与仅对初始数组执行简单的 contains 相比,它非常慢。
为什么这么慢?
上面的代码有什么问题?
参考资料
编辑:这个问题具体不是关于 .contains() ,而是可能是其他数组函数,例如.indexOf()、.map()、forEach() 等。为什么在 web worker 之间拆分工作需要更长的时间...
与简单的 contains
相比,您在 t0
和 t1
之间做了更多的工作。这些额外的步骤包括:
- 转换函数 -> 字符串 -> 正则表达式 -> blob -> 对象 URL
- 调用新的worker -> 解析对象URL -> JS引擎解释代码
- 发送 web 工作数据 -> 在主线程上序列化 -> 在 worker 中反序列化(可能在实际复制的内存结构中,所以不是很慢)
你最好先创建线程,然后不断地向它传递数据。它可能不会更快,但不会锁定您的 UI。
此外,如果您反复搜索数组,我建议您将其转换为一个映射,其中键是数组值,值是索引。
例如
数组 ['apple', 'coconut', 'kiwi']
将转换为 { apple: 1, coconut: 2, kiwi:3 }
通过地图搜索将在分摊的正常时间内发生(快速),而数组将是线性搜索(对于大型集合来说非常慢)。
这是一个有点人为的例子,因此很难帮助优化您正在尝试做的具体事情,但一个容易被忽视且可修复的缓慢路径是将数据复制到网络工作者。如果可能,您可以使用 ArrayBuffers 和 SharedArrayBuffers 来快速地与 web workers 之间传输数据。
您可以使用 postMessage 函数的第二个参数将 arrayBuffer 的所有权转移给网络工作者。重要的是要注意,在 web worker 将缓冲区传输 back 之前,主线程将无法再使用该缓冲区。 SharedArrayBuffers 没有此限制,可以同时被许多工作人员读取,但出于安全考虑,不一定在所有浏览器中都受支持(参见 mdn for more details)
例如
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 参数的更多信息 here and transferable types here。重要的是要注意,您的示例进行时间比较的方式也包括 web worker 创建时间,但希望这能更好地了解大部分时间的去向以及如何更好地解决它。
最近我一直在尝试使用Web workers接口在JavaScript.
中试验线程尝试使用网络工作者 contains,执行以下步骤:
- 将初始数组拆分为大小相等的部分
- 为每个运行 .contains 的片段创建一个网络工作者
- 如果在任何片段中找到值,则 returns 为真,无需等待所有 worker 完成。
这是我尝试过的:
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:
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'})); }
任何浏览器和 cpu 都尝试过,与仅对初始数组执行简单的 contains 相比,它非常慢。
为什么这么慢? 上面的代码有什么问题?
参考资料
编辑:这个问题具体不是关于 .contains() ,而是可能是其他数组函数,例如.indexOf()、.map()、forEach() 等。为什么在 web worker 之间拆分工作需要更长的时间...
与简单的 contains
相比,您在 t0
和 t1
之间做了更多的工作。这些额外的步骤包括:
- 转换函数 -> 字符串 -> 正则表达式 -> blob -> 对象 URL
- 调用新的worker -> 解析对象URL -> JS引擎解释代码
- 发送 web 工作数据 -> 在主线程上序列化 -> 在 worker 中反序列化(可能在实际复制的内存结构中,所以不是很慢)
你最好先创建线程,然后不断地向它传递数据。它可能不会更快,但不会锁定您的 UI。 此外,如果您反复搜索数组,我建议您将其转换为一个映射,其中键是数组值,值是索引。
例如
数组 ['apple', 'coconut', 'kiwi']
将转换为 { apple: 1, coconut: 2, kiwi:3 }
通过地图搜索将在分摊的正常时间内发生(快速),而数组将是线性搜索(对于大型集合来说非常慢)。
这是一个有点人为的例子,因此很难帮助优化您正在尝试做的具体事情,但一个容易被忽视且可修复的缓慢路径是将数据复制到网络工作者。如果可能,您可以使用 ArrayBuffers 和 SharedArrayBuffers 来快速地与 web workers 之间传输数据。
您可以使用 postMessage 函数的第二个参数将 arrayBuffer 的所有权转移给网络工作者。重要的是要注意,在 web worker 将缓冲区传输 back 之前,主线程将无法再使用该缓冲区。 SharedArrayBuffers 没有此限制,可以同时被许多工作人员读取,但出于安全考虑,不一定在所有浏览器中都受支持(参见 mdn for more details)
例如
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 参数的更多信息 here and transferable types here。重要的是要注意,您的示例进行时间比较的方式也包括 web worker 创建时间,但希望这能更好地了解大部分时间的去向以及如何更好地解决它。