免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

使用 WasmEdge C API 与 WASM Threads 提案多线程并行渲染 Mandelbrot 集

使用 WasmEdge C API 与 WASM Threads 提案多线程并行渲染 Mandelbrot 集 使用 WasmEdge C API 与 WASM Threads 提案多线程并行渲染 Mandelbrot 集【免费下载链接】WasmEdgeWasmEdge is a lightweight, high-performance, and extensible WebAssembly runtime for cloud native, edge, and decentralized applications. It powers serverless apps, embedded functions, microservices, smart contracts, and IoT devices.项目地址: https://gitcode.com/GitHub_Trending/wa/WasmEdgeMandelbrot 集渲染是典型的计算密集型任务本文基于 WasmEdge 仓库中的 mandelbrot-set-in-threads 示例完整演示如何借助 WasmEdge C API 与 WASM Threads 提案将单一 WASM 模块加载到多个std::thread工作线程中通过共享内存并行渲染 1200×800 的 Mandelbrot 图像并与 NodeJS V8 Worker Threads 的等价实现做逐线程数性能对比。读完本文你将掌握把 C 代码编译为支持共享内存的 WASM 并 AOT 化为原生共享库的完整工具链、用 WasmEdge C API 创建可共享内存与注册宿主模块的方法以及一套可复现的线程可扩展性基准测试流程。示例背景与设计思路Mandelbrot 集渲染需要对每个像素执行复数迭代本项目使用iterateEquationmandelbrot.c计算量随分辨率与迭代次数线性放大天然适合并行加速。该示例的 C 代码源自 ColinEberhardt/wasm-mandelbrot 的 WASM 单线程版本仓库在此基础上将其改造为多工作线程multi-worker版本用两套运行时做对照运行时编译/执行方式并行手段WasmEdgeC 源码经clang编译为 WASM再用 AOT 编译器转成本地共享库Cstd::thread WasmEdge C APINodeJS同一份 WASM 二进制由 V8 WebAssembly 引擎解释/编译执行JS Worker Threads两套实现共享同一份mandelbrot.wasm与同一份宿主侧共享内存每个工作线程只负责渲染图像在 y 方向上的一个条带stride所有线程并发写入同一块共享内存中的不同区域从而实现无锁并行。从 C 源码到支持共享内存的 WASM渲染核心与分条带策略mandelbrot.c 定义了 1200×800、每像素 RGBA 四字节的图像缓冲#define WIDTH 1200 #define HEIGHT 800 #define ImageSize (WIDTH * HEIGHT * 4) unsigned char Image[ImageSize];单线程版本mandelbrot逐像素扫描全图多线程版本mandelbrotThread则接收num_threads与rank线程序号自行推导自己在 y 方向上的条带边界void mandelbrotThread(int MaxIterations, int NumThreads, int Rank, double Cx, double Cy, double Diameter) { for (int X 0; X WIDTH; X) { int YStride (HEIGHT NumThreads - 1) / NumThreads; // ceil(HEIGHT, NumThreads) int YOffset YStride * Rank; int YMax (YOffset YStride HEIGHT) ? HEIGHT : YOffset YStride; for (int Y YOffset; Y YMax; Y) { // 计算该像素的 Mandelbrot 迭代次数并写入 Image[...] } } }其中YStride用(HEIGHT num_threads - 1) / num_threads实现向上取整YMax做越界钳制保证线程数不能整除图像高度时也能正确收尾。所有线程写入Image的不同条带区域天然无数据竞争无需原子指令或互斥锁。编译与链接--import-memory与--shared-memory为了让多个 WASM 实例共享同一块内存WASM 模块自身不能定义内存而必须导入宿主侧创建的内存。因此链接时需显式打开共享内存相关特性Makefilemandelbrot.o: mandelbrot.c clang -O3 --no-standard-libraries --targetwasm32 -c -o $ $^ mandelbrot.wasm: mandelbrot.o wasm-ld --no-entry $^ -o $ --import-memory --export-all --shared-memory \ --featuresmutable-globals,atomics,bulk-memory,multivalue,reference-types,\ sign-ext,call-indirect-overlong,nontrapping-fptoint,bulk-memory-opt要点说明--import-memory声明内存由宿主导入模块内不再自行分配--shared-memory开启共享内存语义配合--featuresatomics使内存可被多实例并发访问--export-all导出全部符号含mandelbrotThread、getImage供宿主按名调用--features...一次性启用 WASM 线程提案及配套的后缀特性集合。完成编译后可用wasm2wat --enable-allMakefile 的mandelbrot.wat目标反汇编检查内存确实位于{env: memory}导入段这是后续宿主注册模块时必须匹配的名称。AOT 编译为共享库WASM 二进制默认由解释器执行对于计算密集型负载示例明确推荐进一步用 WasmEdge AOT 编译器把 WASM 转成本地共享库mandelbrot.sowasmedgec --enable-threads mandelbrot.wasm mandelbrot.soMakefile 中的等价写法为wasmedge compile --enable-threads mandelbrot.wasm mandelbrot.soMakefile。--enable-threads对应 WasmEdge 的 Threads 提案支持是共享内存与多线程执行的前置条件。用 WasmEdge C API 构造多线程运行时宿主程序 main.cc 完整演示了从配置、内存实例、模块注册到线程调度的全流程。第一步配置 VM 并开启 Threads 提案与 AOT 模式WasmEdge_ConfigureContext *ConfCxt WasmEdge_ConfigureCreate(); WasmEdge_ConfigureAddProposal(ConfCxt, WasmEdge_Proposal_MultiMemories); WasmEdge_ConfigureRemoveProposal(ConfCxt, WasmEdge_Proposal_ReferenceTypes); WasmEdge_ConfigureAddProposal(ConfCxt, WasmEdge_Proposal_Threads); WasmEdge_ConfigureSetRunMode(ConfCxt, WasmEdge_RunMode_AOT); WasmEdge_VMContext *VMCxt WasmEdge_VMCreate(ConfCxt, NULL);WasmEdge_ConfigureSetRunMode(ConfCxt, WasmEdge_RunMode_AOT)必须在WasmEdge_VMCreate之前设置否则无法加载 AOT 编译产生的原生共享库。第二步创建可共享的宿主内存实例关键点在于用WasmEdge_LimitCreateWithMax的第 4 个参数IsSharedtrue创建共享内存类型。与普通内存不同共享内存必须显式指定 maximum 上限此处 minmax60 页一页 64 KiB共约 3.75 MiB足够容纳 1200×800×4 字节的图像缓冲WasmEdge_LimitContext *LimitCxt WasmEdge_LimitCreateWithMax(60, 60, false, true); WasmEdge_MemoryTypeContext *MemTypeCxt WasmEdge_MemoryTypeCreate(LimitCxt);其签名与语义见 wasmedge_ast.hIs64Bit取false表示 32 位地址空间IsShared取true表示该限制用于共享内存类型用于表类型时该参数必须为false。第三步注册{env: memory}宿主模块WASM 导入的内存位于模块{env: memory}由--import-memory决定可通过反汇编确认。宿主需要构造同名模块实例并挂载内存实例WasmEdge_String ExportName WasmEdge_StringCreateByCString(env); WasmEdge_ModuleInstanceContext *HostModCxt WasmEdge_ModuleInstanceCreate(ExportName); WasmEdge_MemoryInstanceContext *HostMemory WasmEdge_MemoryInstanceCreate(MemTypeCxt); WasmEdge_String MemoryName WasmEdge_StringCreateByCString(memory); WasmEdge_ModuleInstanceAddMemory(HostModCxt, MemoryName, HostMemory); WasmEdge_VMRegisterModuleFromImport(VMCxt, HostModCxt);WasmEdge_MemoryInstanceCreatewasmedge_instance.h按内存类型实例化内存WasmEdge_VMRegisterModuleFromImport将宿主模块注入 VM 的导入命名空间。此后所有加载该 WASM 的工作线程看到的都是同一块HostMemory。第四步注册 AOT 共享库WasmEdge_String ModName WasmEdge_StringCreateByCString(mandelbrot); Res WasmEdge_VMRegisterModuleFromFile(VMCxt, ModName, ./mandelbrot.so);WasmEdge_VMRegisterModuleFromFilewasmedge_vm.h将磁盘上的.so或.wasm注册为 VM 中命名模块。若直接加载mandelbrot.wasm而非 AOT 共享库WasmEdge 将以解释器模式执行功能一致但性能更差——这正是文中用WasmEdge-Interp作为对照组的原因。第五步多线程并发调用 WASM 函数由于WasmEdge_VMExecuteRegistered是线程安全的宿主可放心地把同一 VM 交给多个std::thread并发调用。每个线程根据自己的Rank计算条带并渲染WasmEdge_String FuncName WasmEdge_StringCreateByCString(mandelbrotThread); std::vectorstd::thread Threads; for (int Tid 0; Tid NumThreads; Tid) { Threads.push_back(std::thread( { WasmEdge_Value Params[6] { WasmEdge_ValueGenI32(MaxIterations), WasmEdge_ValueGenI32(NumThreads), WasmEdge_ValueGenI32(Rank), WasmEdge_ValueGenF64(X), WasmEdge_ValueGenF64(Y), WasmEdge_ValueGenF64(D), }; WasmEdge_VMExecuteRegistered(VMCxt, ModName, FuncName, Params, 6, NULL, 0); }, Tid)); } for (auto Thread : Threads) { Thread.join(); }六个参数与 WASM 侧mandelbrotThread的形参一一对应MaxIterations10000图像中心坐标X-0.743644786、Y0.1318252536、直径D0.00029336main.cc。线程数由命令行第一个参数传入缺省为 4。并行渲染完成后宿主调用getImage获得图像缓冲在共享内存中的偏移量再用WasmEdge_MemoryInstanceGetDatawasmedge_instance.h把 1200×800×4 字节拷回宿主缓冲区并写入output-wasmedge.bin。对照组NodeJS V8 多 Worker 实现NodeJS 侧用WebAssembly.Memory以同样的参数initialmaximum60、sharedtrue创建共享内存main.js通过postMessage语义把内存与任务参数传给每个 Worker// main.js —— 创建 N 个 Worker const { Worker } require(worker_threads); const worker new Worker(./worker.js, { workerData: { data: { memory, config, num_threads, rank, bytes } }, }); worker.on(message, (offset) { /* 收集完成信号、导出结果 */ });// worker.js —— 每个 Worker 实例化同一 WASM 并渲染自己的条带 WebAssembly.instantiate(bytes, { env: { memory } }).then((Module) { Module.instance.exports.mandelbrotThread( config.iterations, num_threads, rank, config.x, config.y, config.d ); parentPort.postMessage(Module.instance.exports.getImage()); });可以看到两套实现的结构高度对称共享内存memory 同一 WASM 二进制mandelbrot.wasm 按rank分条带调用mandelbrotThread。差异仅在于线程模型std::threadvs Worker Threads与执行引擎WasmEdge AOT vs V8。注意 NodeJS 侧使用mandelbrot.wasm而非 AOT 共享库因此需要--experimental-wasm-threads --experimental-wasm-bulk-memory实验性 flag见 test.bash。将二进制缓冲转换为 PNG两套实现产出的都是裸 RGBA 二进制文件output-wasmedge.bin/output-node.bin。WASM 侧通过getImage暴露缓冲地址unsigned char *getImage() { return Image[0]; }convert.js 使用canvas模块把裸缓冲封装为ImageData并编码为 PNGconst { createCanvas } require(canvas); const canvas createCanvas(1200, 800); const context canvas.getContext(2d); const imageData context.createImageData(1200, 800); imageData.data.set(canvasData); context.putImageData(imageData, 0, 0); const buffer canvas.toBuffer(image/png);转换命令两个运行时各一张node convert.js output-wasmedge.bin output-wasmedge.png node convert.js output-node.bin output-node.png仓库中已附带渲染结果WasmEdge 与 NodeJS 的输出在视觉上完全一致证明两套实现功能等效构建与基准测试流程环境准备与编译示例要求本机已安装 WasmEdge含 C API 库与 LLVM 12 工具链clang/wasm-ld/wasmedgec。安装 WasmEdge 后按以下步骤构建npm install canvas # convert.js 依赖的 PNG 编码模块 make # 依次产出 mandelbrot.o → mandelbrot.wasm → mandelbrot.so → mainmain的编译命令为clang -lwasmedge -stdc17 -pthread -o main main.ccMakefile其中-lwasmedge链接 C API 动态库、-pthread启用 POSIX 线程。一键基准脚本test.bash 自动完成 110 线程的两侧对比测试先用./main {i}跑 10 轮 WasmEdgeAOT再用node --experimental-wasm-threads --experimental-wasm-bulk-memory main.js {i}跑 10 轮 NodeJS分别 grep 出Elapsed Time汇总最后把两个.bin转换为 PNGbash test.bashWasmEdge 侧单次运行会打印每个线程的调用结果与总耗时./main 4性能结果与线程可扩展性分析示例在Intel(R) Xeon(R) Gold 6226R CPU与node v14.18.2上用同一份 WASM 二进制对三套配置做了测试WasmEdge-AOT加载mandelbrot.so、WasmEdge-Interp加载mandelbrot.wasm与NodeJSV8 加载mandelbrot.wasm。核心结论如下单线程下WasmEdge-AOT比NodeJS快1.27×说明该负载下 WasmEdge AOT 编译器优于 NodeJS V8 的 wasm 编译器多线程下WasmEdge-AOT的线程可扩展性优于多 Worker 的NodeJS10 线程时前者获得5.71×加速后者仅4.71×表明 WasmEdge 的线程调度开销更小WasmEdge-Interp的线程可扩展性虽低于 AOT但仍略高于NodeJS印证线程级并行同样可以加速 wasm 解释器。实测耗时表ms线程数WasmEdge-AOTstdevNodeJSstdev1525.608.75668.5117.412287.545.77381.905.993234.842.22323.882.664183.086.02261.051.935159.181.49236.581.766132.434.12206.750.917118.191.64191.051.418108.191.30178.922.20995.050.89167.481.621092.112.27163.581.96以上数据为仓库示例文档记录的原始测量结果测试环境与数据样本有限仅供参考实际表现取决于具体硬件、WasmEdge/NodeJS 版本与负载特征。小结本示例展示了一条完整的计算密集型 WASM 负载并行化路径--import-memory --shared-memory链接 →wasmedgec --enable-threadsAOT 编译 → WasmEdge C API 创建共享内存与宿主模块 →std::thread 线程安全的WasmEdge_VMExecuteRegistered并行调度。相比 NodeJS 的 Worker Threads 方案WasmEdge 方案在同等并行度下展现出更低的线程开销与更好的可扩展性且宿主程序是轻量的 C 代码适合嵌入到边缘计算、Serverless 函数等资源受限场景。读者可直接复用 examples/capi/mandelbrot-set-in-threads 目录下的源码与 Makefile作为自己项目多线程 WASM 集成的起点。【免费下载链接】WasmEdgeWasmEdge is a lightweight, high-performance, and extensible WebAssembly runtime for cloud native, edge, and decentralized applications. It powers serverless apps, embedded functions, microservices, smart contracts, and IoT devices.项目地址: https://gitcode.com/GitHub_Trending/wa/WasmEdge创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表