searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

前端大数据交换如何实现zero copy

2023-10-23 01:45:22
29
0

我们都知道,如果大量的数据计算直接放在js主线程去执行,那么肯定会造成页面假死的状态,这是我们不能容忍的,并且在多可视化图形并行渲染的过程中,甚至导致可视化的动画完全丢失,体验很差。

 

一、数据分片

最简单的方案,就是把这大量的数据进行分片异步处理,处理完一段数据之后,释放主线程,让ui渲染进程有机会能够执行,在回头继续处理第二段数据,直到所有分片执行完成,更新最终UI侧数据

二、将数据直接发送给web worker执行

// 以下为伪代码,了解思想即可
/*
* main.js
*前端fetch到数据之后,直接发送给worker进行计算
*/
const bigData = [] // 500M的数据
const worker = new Worker("./worker.js");
worker.postMessage(bigData)

- 如果直接发送这份数据给到worker,postMessage会对这份数据做一次拷贝,那么浏览器就存储了1G数据,
在拷贝的过程中还会对数据进行序列化,在worker中,还需要对数据反序列化,无疑增加了很多成本

/*
* worker.js
*/
self.onmessage = funcion({ data }){
    // calcData 进行复杂的计算逻辑
}
/*

*/

三、web worker优化方案

有没有办法,能够把发送给worker的这份数据,不要拷贝,而是指针(地址)的方式,worker直接进行计算,然后返回给我们一个标记,我们直接还是调用之前的那份数据呢?是有办法的
worker.postMessage(aMessage, transferList)

这个transferList是一个可选数组,用于传递所有权,什么意思呢,就是发送给worker之前,主线程是可以使用当前这份数据的,当一旦发送给worker之后,就失去了对这份数据的控制权,也就无法进行遍历或者进行分片等操作了,只有等worker处理好之后,在将这份数据发送回来,如果worker异常或者没有发送回来,那么这份数据就将会丢失掉。

好吧,那我就是想用,做好容灾就行。还有一个局限性,我们传递的这份数据是要实现Transferable接口的,目前前端实现这个接口的数据结构有三个,ArrayBuffer,MessagePort,ImageBitmap,那我们就拿ArrayBuffer来举例吧(只对它有所了解)

那看来,如果我们要使用ArrayBuffer,就要对数据进行约束了,因为二进制数组存放的都是二进制数据,并且支持的类型有以下几种
这里插入图片

main.js
const bigData = [] // 500M的数据
const worker = new Worker("./worker.js");
/*
把原始数据结构中我们所需要的数据提取出来
*/

worker.postMessage(bigData)

/*
* worker.js
*/
self.onmessage = function({ data }){
    const { buffer } = data;
    // 如果要操作这份数据,在创建一个view视图,具体typedArray的操作api,可以查看MDN文档
    // 创建视图,简单来说,就是去观察二进制数组buffer中的数据,并不是实例化拷贝一份数据出来,buffer中的数据一个黑盒,不能直接进行操作,要先定义视图并且赋予期数据类型,才可进行操作
    const view = new Int32Array(buffer)
    // 对view进行复杂操作,返回给主线程进行读取
    self.postMessage(view,[view.buffer]);
}

四、共享内存sharedArrayBuffer

上面的方案,我们发现一个问题,就是数据控制权的问题,以及虽然没有发生数据拷贝(相当于剪切),耗用的内存是不变的,更进一步来说,能不能直接把内存地址给到worker,woker直接去操作这份数据呢?

const worker = new Worker("./worker.js");
// 实例化共享内存
const buffer = new SharedArrayBuffer(arr.legnth * Int32Array.BYTES_PER_ELEMENT);
worker.postMessage({ buffer });
worker.onmessage = function () {
    const view = new Int32Array(buffer);
    console.log(buffer, view);
}

/*
注意:因为SharedArrayBuffer存在安全漏洞的原因,在2018年被叫停了,目前只能在chrome和Mozilla中开启一定的安全策略下使用,可以先了解,未来可期
开启SharedArrayBuffer的两种方法
1.开启 Cross-Origin-Opener-Policy: same-origin
2.在命令窗口,启动chrome --enable-features=SharedArrayBuffer,仅用于调试
3.在https://developer.chrome.com/origintrials/#/registration,注册,返回一个临时token,可用于调试
*/

这样就已经实现了在worker直接分发数据零拷贝,每个不同weorker直接将接受到的数据,进行处理,修改buffer,处理完成之后,告诉主进程已完成done,主进程进行ui更新渲染即可,不过目前SharedArrayBuffe不能用于生产环境。

五、webassembly方案

整体实现思路:

1.调用webAssembly的方法,在webAssembly内存中分配空间,返回指针

2.JS端在webAssembly的memory内存中申请一段arrayBuffer,根据指针位置和数据量建立view视图,并且把数据写入arrayBuffer中

3.调用webAssembly方法完成计算(将步骤一中申请的内存指针传入,再返回计算完成的批量结果的指针位置和大小

4.JS端在webAssembly的memory arraybuffer上,按指针位置和数据量建立view,把数据读出,进行UI渲染

以下是代码的具体实现

#include <emscripten.h>
#include <iostream>
#include <algorithm>
#include <time.h>
using namespace std;
#ifdef __cplusplus
extern "C"
{ // C++ compiler 不会优化函数名,导出到js中,能够按照原始名称导出
#endif
   EMSCRIPTEN_KEEPALIVE int main()
   {
       return 100;
   }

   EMSCRIPTEN_KEEPALIVE void sortArr(int *ptr)
   {
       sort(ptr, ptr + 9);
   }
   EMSCRIPTEN_KEEPALIVE void randomArr(int *ptr)
   {
       srand((unsigned)time(NULL));
       for (int i = 0; i < 9; i++)
       {
           ptr[i] = rand();
       }
   }
   // 这里进行内存申请,其实调用的是wasm memory的线性内存申请
   EMSCRIPTEN_KEEPALIVE int *getArrPointer(int capacity)
   {
       int *prt = (int *)malloc(sizeof(int) * capacity);
       cout << "c++申请的内存地址为:" << prt << endl;
       return prt;
   }
#ifdef __cplusplus
}
#endif

使用emcc编译工具进行编译,得到一个js胶水文件,和一个.wasm文件

// 加载胶水文件,会得到一个Module对象,具体api细节,可网上查阅
const self = this;
Module.onRuntimeInitialized = function () { // 初始化之后
    const bigData = [];
    // 申请内存,返回指针
    const dataPointer = Module._getArrPointer(bigData.length);
    // 从memory中读取数据块,注意,这里一定要写偏移量dataPointer
    const dataview = new Int32Array(Module['asm']['memory'].buffer, dataPointer, bigData.length)
    for (let i = 0; i < bigData.length; i++) {
        dataview[i] = bigData[i];
    }
    // c++业务逻辑处理
    Module._sortArr(dataPointer);
    // 处理完成新建一个view读取,返回给UI进行渲染
}

六、其他调研以及一些想法

1.用sharedArrayBuffer初始化个wasm实例,用worker进行初始化多个wasm实例,共享一片内存

#include <stdlib.h>
#include <emscripten.h>

#ifdef __cplusplus
extern "C"
{ // So that the C++ compiler does not rename the functions below
#endif

    EMSCRIPTEN_KEEPALIVE int *getArrPointer(int capacity)
    {
        int *prt = (int *)malloc(sizeof(int) * capacity);
        prt[0] = 10;
        return prt;
    }

#ifdef __cplusplus
}
#endif

实例化wasm
const memory = 
new WebAssembly.Memory({
    initial: 80,
    maximum: 80,
    shared: true
});

// 编译阶段,采用emcc change1.cpp -O3 -o change1.wasm  --no-entry -Wl,--import-memory,
 * @param {*} e 
 * 1、加载wasm文件
 * 2、使用主进程传入的memory对象,进行初始化wasm,调用c++函数,申请内存,返回一个指针
 * 3、
 */
self.onmessage = function ({ data }) {
    const { memory } = data;
    // 使用这个meory进行wasm的初始化,共享这片内存
    console.log("worker 执行了", memory)
    fetch("change1.wasm").then(response =>
        response.arrayBuffer()
    ).then(bytes => {
        return WebAssembly.instantiate(bytes, {
            env: {
                '__table_base': 0,
                'memory': memory,
                '__memory_base': 1024,
                'STACKTOP': 0,
                'STACK_MAX': memory.buffer.byteLength,
            }
        })
    }
    ).then(({ instance }) => {
        console.log("实例化后的wasm对象", instance);
        console.log("传入对象的memory", memory)
        // const dataPointer = instance.exports.getArrPointer(10);
        // console.log("worker1中的申请的内存为:", dataPointer);
        // self.postMessage(dataPointer)
    });
}

上述方案其实比较完美,不过遇到了一个问题:

LinkError: WebAssembly.instantiate(): mismatch in shared state of memory, declared = 0, imported = 1

好像提示是使用了两个不同内存实例,目前还比较困惑,没找到合理的解释,有进展后续更新~

以上是做的一些调研,下面总结一下

1、如果非必要的情况,不建议使用wasm这种解决方案,可以简单用web worker实现,市面上很多都是一些简单的理论,实践起来坑比较多,成本也很高。

2、如果使用webassembly,那么使用方案五是目前我认为比较好的实践方案,方案六可能是未来的最优解

0条评论
作者已关闭评论
张****伟
7文章数
0粉丝数
张****伟
7 文章 | 0 粉丝
原创

前端大数据交换如何实现zero copy

2023-10-23 01:45:22
29
0

我们都知道,如果大量的数据计算直接放在js主线程去执行,那么肯定会造成页面假死的状态,这是我们不能容忍的,并且在多可视化图形并行渲染的过程中,甚至导致可视化的动画完全丢失,体验很差。

 

一、数据分片

最简单的方案,就是把这大量的数据进行分片异步处理,处理完一段数据之后,释放主线程,让ui渲染进程有机会能够执行,在回头继续处理第二段数据,直到所有分片执行完成,更新最终UI侧数据

二、将数据直接发送给web worker执行

// 以下为伪代码,了解思想即可
/*
* main.js
*前端fetch到数据之后,直接发送给worker进行计算
*/
const bigData = [] // 500M的数据
const worker = new Worker("./worker.js");
worker.postMessage(bigData)

- 如果直接发送这份数据给到worker,postMessage会对这份数据做一次拷贝,那么浏览器就存储了1G数据,
在拷贝的过程中还会对数据进行序列化,在worker中,还需要对数据反序列化,无疑增加了很多成本

/*
* worker.js
*/
self.onmessage = funcion({ data }){
    // calcData 进行复杂的计算逻辑
}
/*

*/

三、web worker优化方案

有没有办法,能够把发送给worker的这份数据,不要拷贝,而是指针(地址)的方式,worker直接进行计算,然后返回给我们一个标记,我们直接还是调用之前的那份数据呢?是有办法的
worker.postMessage(aMessage, transferList)

这个transferList是一个可选数组,用于传递所有权,什么意思呢,就是发送给worker之前,主线程是可以使用当前这份数据的,当一旦发送给worker之后,就失去了对这份数据的控制权,也就无法进行遍历或者进行分片等操作了,只有等worker处理好之后,在将这份数据发送回来,如果worker异常或者没有发送回来,那么这份数据就将会丢失掉。

好吧,那我就是想用,做好容灾就行。还有一个局限性,我们传递的这份数据是要实现Transferable接口的,目前前端实现这个接口的数据结构有三个,ArrayBuffer,MessagePort,ImageBitmap,那我们就拿ArrayBuffer来举例吧(只对它有所了解)

那看来,如果我们要使用ArrayBuffer,就要对数据进行约束了,因为二进制数组存放的都是二进制数据,并且支持的类型有以下几种
这里插入图片

main.js
const bigData = [] // 500M的数据
const worker = new Worker("./worker.js");
/*
把原始数据结构中我们所需要的数据提取出来
*/

worker.postMessage(bigData)

/*
* worker.js
*/
self.onmessage = function({ data }){
    const { buffer } = data;
    // 如果要操作这份数据,在创建一个view视图,具体typedArray的操作api,可以查看MDN文档
    // 创建视图,简单来说,就是去观察二进制数组buffer中的数据,并不是实例化拷贝一份数据出来,buffer中的数据一个黑盒,不能直接进行操作,要先定义视图并且赋予期数据类型,才可进行操作
    const view = new Int32Array(buffer)
    // 对view进行复杂操作,返回给主线程进行读取
    self.postMessage(view,[view.buffer]);
}

四、共享内存sharedArrayBuffer

上面的方案,我们发现一个问题,就是数据控制权的问题,以及虽然没有发生数据拷贝(相当于剪切),耗用的内存是不变的,更进一步来说,能不能直接把内存地址给到worker,woker直接去操作这份数据呢?

const worker = new Worker("./worker.js");
// 实例化共享内存
const buffer = new SharedArrayBuffer(arr.legnth * Int32Array.BYTES_PER_ELEMENT);
worker.postMessage({ buffer });
worker.onmessage = function () {
    const view = new Int32Array(buffer);
    console.log(buffer, view);
}

/*
注意:因为SharedArrayBuffer存在安全漏洞的原因,在2018年被叫停了,目前只能在chrome和Mozilla中开启一定的安全策略下使用,可以先了解,未来可期
开启SharedArrayBuffer的两种方法
1.开启 Cross-Origin-Opener-Policy: same-origin
2.在命令窗口,启动chrome --enable-features=SharedArrayBuffer,仅用于调试
3.在https://developer.chrome.com/origintrials/#/registration,注册,返回一个临时token,可用于调试
*/

这样就已经实现了在worker直接分发数据零拷贝,每个不同weorker直接将接受到的数据,进行处理,修改buffer,处理完成之后,告诉主进程已完成done,主进程进行ui更新渲染即可,不过目前SharedArrayBuffe不能用于生产环境。

五、webassembly方案

整体实现思路:

1.调用webAssembly的方法,在webAssembly内存中分配空间,返回指针

2.JS端在webAssembly的memory内存中申请一段arrayBuffer,根据指针位置和数据量建立view视图,并且把数据写入arrayBuffer中

3.调用webAssembly方法完成计算(将步骤一中申请的内存指针传入,再返回计算完成的批量结果的指针位置和大小

4.JS端在webAssembly的memory arraybuffer上,按指针位置和数据量建立view,把数据读出,进行UI渲染

以下是代码的具体实现

#include <emscripten.h>
#include <iostream>
#include <algorithm>
#include <time.h>
using namespace std;
#ifdef __cplusplus
extern "C"
{ // C++ compiler 不会优化函数名,导出到js中,能够按照原始名称导出
#endif
   EMSCRIPTEN_KEEPALIVE int main()
   {
       return 100;
   }

   EMSCRIPTEN_KEEPALIVE void sortArr(int *ptr)
   {
       sort(ptr, ptr + 9);
   }
   EMSCRIPTEN_KEEPALIVE void randomArr(int *ptr)
   {
       srand((unsigned)time(NULL));
       for (int i = 0; i < 9; i++)
       {
           ptr[i] = rand();
       }
   }
   // 这里进行内存申请,其实调用的是wasm memory的线性内存申请
   EMSCRIPTEN_KEEPALIVE int *getArrPointer(int capacity)
   {
       int *prt = (int *)malloc(sizeof(int) * capacity);
       cout << "c++申请的内存地址为:" << prt << endl;
       return prt;
   }
#ifdef __cplusplus
}
#endif

使用emcc编译工具进行编译,得到一个js胶水文件,和一个.wasm文件

// 加载胶水文件,会得到一个Module对象,具体api细节,可网上查阅
const self = this;
Module.onRuntimeInitialized = function () { // 初始化之后
    const bigData = [];
    // 申请内存,返回指针
    const dataPointer = Module._getArrPointer(bigData.length);
    // 从memory中读取数据块,注意,这里一定要写偏移量dataPointer
    const dataview = new Int32Array(Module['asm']['memory'].buffer, dataPointer, bigData.length)
    for (let i = 0; i < bigData.length; i++) {
        dataview[i] = bigData[i];
    }
    // c++业务逻辑处理
    Module._sortArr(dataPointer);
    // 处理完成新建一个view读取,返回给UI进行渲染
}

六、其他调研以及一些想法

1.用sharedArrayBuffer初始化个wasm实例,用worker进行初始化多个wasm实例,共享一片内存

#include <stdlib.h>
#include <emscripten.h>

#ifdef __cplusplus
extern "C"
{ // So that the C++ compiler does not rename the functions below
#endif

    EMSCRIPTEN_KEEPALIVE int *getArrPointer(int capacity)
    {
        int *prt = (int *)malloc(sizeof(int) * capacity);
        prt[0] = 10;
        return prt;
    }

#ifdef __cplusplus
}
#endif

实例化wasm
const memory = 
new WebAssembly.Memory({
    initial: 80,
    maximum: 80,
    shared: true
});

// 编译阶段,采用emcc change1.cpp -O3 -o change1.wasm  --no-entry -Wl,--import-memory,
 * @param {*} e 
 * 1、加载wasm文件
 * 2、使用主进程传入的memory对象,进行初始化wasm,调用c++函数,申请内存,返回一个指针
 * 3、
 */
self.onmessage = function ({ data }) {
    const { memory } = data;
    // 使用这个meory进行wasm的初始化,共享这片内存
    console.log("worker 执行了", memory)
    fetch("change1.wasm").then(response =>
        response.arrayBuffer()
    ).then(bytes => {
        return WebAssembly.instantiate(bytes, {
            env: {
                '__table_base': 0,
                'memory': memory,
                '__memory_base': 1024,
                'STACKTOP': 0,
                'STACK_MAX': memory.buffer.byteLength,
            }
        })
    }
    ).then(({ instance }) => {
        console.log("实例化后的wasm对象", instance);
        console.log("传入对象的memory", memory)
        // const dataPointer = instance.exports.getArrPointer(10);
        // console.log("worker1中的申请的内存为:", dataPointer);
        // self.postMessage(dataPointer)
    });
}

上述方案其实比较完美,不过遇到了一个问题:

LinkError: WebAssembly.instantiate(): mismatch in shared state of memory, declared = 0, imported = 1

好像提示是使用了两个不同内存实例,目前还比较困惑,没找到合理的解释,有进展后续更新~

以上是做的一些调研,下面总结一下

1、如果非必要的情况,不建议使用wasm这种解决方案,可以简单用web worker实现,市面上很多都是一些简单的理论,实践起来坑比较多,成本也很高。

2、如果使用webassembly,那么使用方案五是目前我认为比较好的实践方案,方案六可能是未来的最优解

文章来自个人专栏
前端数据处理
7 文章 | 1 订阅
0条评论
作者已关闭评论
作者已关闭评论
0
0