WebAssembly筛选函数比JS慢10倍?求原因及优化方案
WebAssembly筛选性能劣于JS的原因及优化方案
问题背景
作为WebAssembly新手,我实现了一个根据条件筛选对象数组并返回ID数组的函数,测试发现Wasm版本的执行速度比JS慢10倍——处理10万条数据时JS耗时约1秒,Wasm耗时约10秒。
相关代码与编译命令
JS筛选函数
function filter(array: Obj[], criteria: Filter, additionalArr: number[]){//Filter逻辑}
C++代码
#include<emscripten/bind.h> #include<emscripten/val.h> struct Filter { vector<int> f1; vector<int> f2; vector<string> f3; Filter(): f1({}), f2({}), f3({}) {} Filter(vector<int> f1, vector<int> f2, vector<string> f3) : f1(f1), f2(f2), f3(f3) {} }; struct Obj { int p1; int p2; string p3; Obj (): p1(0), p2(0), p3("") {} Obj (int p1, int p2, string p3) : p1(p1), p2(p2), p3(p3) {} }; Obj processObj(emscripten::val jsObj){ Obj wasmObj = new Obj(); wasmObj.p1 = jsObj["p1"].as<int>(); wasmObj.p2 = jsObj["p2"].as<int>(); wasmObj.p3 = jsObj["p3"].as<string>(); return wasmObj; } Filter processFilter(emscripten::val jsFilter){ //与processObj逻辑类似} bool filterOneObj(Obj obj, Filter criterias, vector<int> additionalArr) { //单对象筛选逻辑} emscripten::val wasmFilter(emscripten::val objJS, emscripten::val criteriaJS, emscripten:: val additionalArrJS) { vector<int> result; Filter wasmFilter = processFilter(criteriaJS); for(size_t i = 0; i < objJS["length"].as<size_t>(); i++) { if(filterOneObj()) result.push_back(objJS[i]["p1"].as<int>()); } return val::array(result); } EMSCRIPTEN_BINDINGS(module) { class_<Filter>("WasmFilter") .constructor<vector<int>,vector<int>,vector<string>>() .property("f1", &Filter::f1) .property("f2", &Filter::f2) .property("f3", &Filter::f3); class_<Obj>("WasmObj")...//与Filter绑定逻辑类似 emscripten::register_vector<int>("IntList"); emscripten::register_vector<Obj>("ObjList"); emscripten::function("wasmFilter", &wasmFilter); }
编译命令
emcc -lembind -Iinclude -s WASM=1 -s EXPORT_ES6=1 -s MODULARIZE=1 -s ENVIRONMENT=web filter.cpp -o filter.js
(注:原命令中-s WASM=应为-s WASM=1,否则不会生成Wasm文件)
核心疑问
- Wasm性能远低于JS的原因是否和使用embind有关?
- JS与C++之间传递对象和数组的高效方法是什么?
原因解析
Wasm版本性能差的核心问题确实和embind的频繁跨语言交互直接相关,具体有这些关键点:
- 循环内的频繁边界切换:遍历10万条数据时,每次循环都通过
objJS[i]["p1"].as<int>()访问JS对象属性,这会触发大量JS-Wasm边界切换——这种切换本身开销极高,10万次累加后直接拖慢整体速度。 - embind的类型转换开销:embind在JS对象与C++结构体之间转换时,需要做类型检查、内存分配和数据拷贝,比如
processObj逐个转换JS对象,以及val::array(result)转换结果数组的过程,都会产生额外开销。 - 不必要的数据拷贝:
filterOneObj函数直接传递Filter和vector<int>的副本而非引用,导致额外的内存拷贝,进一步降低性能。 - 编译未开启优化:原编译命令未加优化参数,C++代码未得到充分优化,也会影响执行效率。
高效跨语言传递方案
优化的核心思路是减少跨语言边界交互次数,一次性传递批量数据,避免循环内的频繁切换。推荐以下方案:
1. 使用TypedArray传递结构化数据
将JS中的对象数组转换为连续内存的TypedArray(如Uint32Array、Uint8Array),直接传递内存指针给Wasm,让C++直接操作共享内存:
- 数值类型(p1、p2):用两个
Uint32Array分别存储所有对象的p1和p2值; - 字符串类型(p3):将所有字符串拼接成一个大的
Uint8Array,再用Uint32Array存储每个字符串的起始偏移和长度。
C++通过指针直接访问这些内存,完全避免类型转换开销。
2. 手动管理WebAssembly共享内存
手动创建WebAssembly.Memory对象,让JS和Wasm共享同一块内存区域:
- JS将数据写入共享内存后,把内存地址传递给Wasm;
- Wasm直接读取内存地址处理数据;
- 处理完成后,JS从共享内存读取结果。
这种方式完全绕开embind的类型转换,是性能最高的跨语言数据传递方式。
3. 优化embind的使用(若必须保留)
如果一定要用embind,至少做这些优化:
- 批量转换数据:不要在循环内逐个转换JS对象,而是先一次性将整个JS数组转为C的
vector<Obj>,再在C内遍历处理,减少跨语言交互次数; - 传递引用而非副本:修改
filterOneObj的参数为const Filter& criterias和const vector<int>& additionalArr,避免不必要的拷贝; - 开启编译优化:编译时添加
-O3参数,让Emscripten对C++代码做深度优化,修改后的编译命令:emcc -O3 -lembind -Iinclude -s WASM=1 -s EXPORT_ES6=1 -s MODULARIZE=1 -s ENVIRONMENT=web filter.cpp -o filter.js
4. 避免循环内的JS对象访问
在wasmFilter函数开头就将整个objJS数组读取到C容器中,遍历C容器而非每次循环访问JS对象,彻底消除循环内的跨语言交互开销。
内容的提问来源于stack exchange,提问作者Bingo213
相关产品推荐
相关产品推荐

