得物技术 2024年11月05日
基于IM场景下的Wasm初探:提升Web应用性能|得物技术
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

WebAssembly是一种可移植、体积小、加载快速的二进制格式,可提高Web应用性能,本文介绍了其原理、优势、限制、应用场景、产品案例及实践案例等内容。

WebAssembly是一种二进制指令格式,可将多种编程语言编译成Wasm模块,在现代浏览器中运行,尤其在GPU或CPU计算时优势明显。

需要Wasm是因为JavaScript作为解释型语言执行速度慢,Wasm具有紧凑二进制格式、可与JavaScript共存等优势,但也存在生态不完善等局限性。

Wasm工作原理是提前编译为WebAssembly二进制格式,浏览器加载时验证合法性并编译为机器码,其应用场景包括高性能计算、游戏开发等。

通过实践案例展示了Rust + Wasm实现数据调用的过程,包括环境配置、项目初始化、编写代码、执行编译等步骤。

原创 WWQ 2024-11-04 18:30 上海

Wasm是一种可移植、体积小、加载快速的二进制格式,可以将各种编程语言的代码编译成Wasm模块,这些模块可以在现代浏览器中直接运行。尤其在涉及到GPU或CPU计算时优势相对比较明显。

目录

一、何为Wasm ?

二、为什么需要Wasm ?

三、优势和限制

四、Wasm工作原理

五、应用场景

六、产品案例

七、实践案例

    1. Rust项目初始化

    2. 配置包文件

    3. 编写代码

    4. 执行编译

    5. 在前端项目中引入使用

    6. 性能比较

八、总结


何为Wasm ?

Wasm,全称 WebAssembly,官网描述是一种用于基于堆栈的虚拟机的二进制指令格式。Wasm被设计为一个可移植的目标,用于编译C/C++/Rust等高级语言,支持在Web上部署客户端和服务器应用程序。


Wasm 的开发者参考文档:

https://developer.mozilla.org/en-US/docs/WebAssembly


简单的来说就是使用C/C++/Rust等语言编写的代码,经过编译后得到汇编指令,再通过JavaScript相关API将文件加载到Web容器中,一句话解释就是运行在Web容器中的汇编代码。Wasm是一种可移植、体积小、加载快速的二进制格式,可以将各种编程语言的代码编译成Wasm模块,这些模块可以在现代浏览器中直接运行。尤其在涉及到GPU或CPU计算时优势相对比较明显。


为什么需要Wasm ?

JavaScript是解释型语言,相比于编译型语言需要在运行时转换,所以解释型语言的执行速度要慢于编译型语言。


编译型语言和解释型语言代码执行的大致流程如下:


如上流程图所示,解释型语言每次执行都需要把源码转换一次才能执行,而转换过程非常耗费时间和性能,所以在 JavaScript背景下,Web执行一些高性能应用是非常困难的,如视频剪辑、3D游戏等。


Wasm具有紧凑的二进制格式,可以接近原生的性能运行,并为C/C++等语言提供一个编译目标,以便它们可以在Web上运行。被设计为可以与JavaScript共存,允许两者一起工作。在特定的业务场景下可以完美的弥补JavaScript的缺陷。


优势和限制

优势:


局限性:


Wasm工作原理

通过上述的编译型语言和解释型语言代码执行的大致流程我们可以知道Wasm是不需要被解释的,是由开发者提前编译为WebAssembly二进制格式,如下图所示。由于变量类型都是预知的,因此浏览器加载WebAssembly文件时,JavaScript引擎无须监测代码。它可以简单地将这段代码的二进制格式编译为机器码。


从这个流程中我们也可以看出,如果将每种编程语言都直接编译为机器码的各个版本,这样效率是不是更高呢?想法是好的,但实现过程确实复杂不堪的。由于浏览器是可以在若干不同的处理器 (比如手机和平板等设备) 上运行,因此为每个可能的处理器发布一个WebAssembly代码的编译后版本会很难做到。


我们可以通过替代方法即取得IR代码。IR即为中间代码(Intermediate Representation),它是编译器中很重要的一种数据结构。编译器在做完前端工作以后,首先就生成IR,并在此基础上执行各种优化算法,最后再生成目标代码。可以简化为如下流程:

编译器将IR代码转换为一种专用字节码并放入后缀为.wasm的文件中。此时Wasm文件中的字节码还不是机器码,它只是支持WebAssembly的浏览器能够理解的一组虚拟指令。当加载到支持WebAssembly的浏览器中时,浏览器会验证这个文件的合法性,然后这些字节码会继续编译为浏览器所运行的设备上的机器码。


更加详情的原理和使用方式可以前往https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface查阅。


应用场景

在Web开发中,可以使用Wasm来提高应用程序的性能。以下是一些使用Wasm的常见场景:


产品案例





实践案例

这里我们通过使用Rust + Wasm实现Wasm与JavaScript之间的数据调用,理解Rust和Wasm的交互过程。


使用Rust就需要做一些前置的环境配置,详情的步骤可参考Rust官网:

https://www.rust-lang.org/zh-CN/tools/install。


安装wasm-pack,wasm-pack是一个构建、测试和发布Wasm的Rust CLI工具,我们将使用wasm-pack相关的命令来构建Wasm二进制内容。这有助于将代码编译为WebAssembly,并生成在浏览器中使用的正确包。


Rust项目初始化

执行cargo new rust_wasm初始化Rust项目,自动生成配置文件Cargo.toml,项目结构如下:

/Users/admin/RustroverProjects/rust_wasm├── Cargo.lock├── Cargo.toml├── src|  └── lib.rs└── target   ├── CACHEDIR.TAG   └── debug      ├── build      ├── deps      ├── examples      └── incremental


配置包文件

我们可以在Cargo.toml文件中加上下列代码并保存,保存之后Cargo会自动下载依赖。

[lib]crate-type = ["cdylib"]
[dependencies]wasm-bindgen = { version = "0.2.89", features = [] }


 编写代码

编写代码之前我们先明确Rust中crate包的概念,Rust中包管理系统将crate包分为二进制包(Binary)和库包(Library)两种,二者可以在同一个项目中同时存在。


二进制包:

库包:

因为我们这里希望将 Wasm 转为一个可以在JS项目中使用的模块,所以需要使用库包 lib.rs 的命名,代码如下。

use wasm_bindgen::prelude::*;#[wasm_bindgen]pub extern "C" fn rust_add(left: i32, right: i32) -> i32 {    println!("Hello from Rust!");    left + right}


执行编译

这里我们要使用到wasm-pack,将上述的Rust代码编译为能够被JS导入的模块,根据wasm-pack提供的target方式可以指定构建的产物,如截图所示:


编译过程效果:


编译完成后,我们会发现根目录下多了一个pkg/ 文件夹,里面就是我们的Wasm产物所在的npm包了。目录结构如下:

/Users/admin/RustroverProjects/rust_wasm/pkg├── package.json├── rust_wasm.d.ts├── rust_wasm.js├── rust_wasm_bg.wasm└── rust_wasm_bg.wasm.d.ts


rust_wasm.d.ts文件内容:

/* tslint:disable *//* eslint-disable *//*** @param {number} num* @returns {string}*/export function msg_insert(num: number): string;/*** @param {number} left* @param {number} right* @returns {number}*/export function rust_add(left: number, right: number): number;/***/export function rust_thread(): void;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput { readonly memory: WebAssembly.Memory; readonly msg_insert: (a: number, b: number) => void; readonly rust_add: (a: number, b: number) => number; readonly rust_thread: () => void; readonly __wbindgen_add_to_stack_pointer: (a: number) => number; readonly __wbindgen_free: (a: number, b: number, c: number) => void;}
export type SyncInitInput = BufferSource | WebAssembly.Module;/*** Instantiates the given `module`, which can either be bytes or* a precompiled `WebAssembly.Module`.** @param {SyncInitInput} module** @returns {InitOutput}*/export function initSync(module: SyncInitInput): InitOutput;
/*** If `module_or_path` is {RequestInfo} or {URL}, makes a request and* for everything else, calls `WebAssembly.instantiate` directly.** @param {InitInput | Promise<InitInput>} module_or_path** @returns {Promise<InitOutput>}*/export default function __wbg_init (module_or_path?: InitInput | Promise<InitInput>): Promise<InitOutput>;


wasm-pack打包不仅输出一个ESM规范的模块,而且还支持自动生成d.ts文件,对模块的使用者非常友好。如下:


在前端项目中引入使用

'use client'/* * @Author: wangweiqiang * @Date: 2024-06-18 17:03:34 * @LastEditors: wangweiqiang * @LastEditTime: 2024-06-18 23:09:55 * @Description: app.tsx */import Image from "next/image";import { useCallback, useEffect, useState } from "react";import init, * as rustLibrary from 'rust_wasm'export default function Home() {  const [addResult, setAddResult] = useState<number | null>(null)  const [calculateTime, setCalculateTime] = useState<string>('')
const initRustLibrary = useCallback(() => { init().then(() => { const result = rustLibrary.rust_add(5, 6) const timeStamp = rustLibrary.msg_insert(50000) setCalculateTime(timeStamp) setAddResult(result) }) }, [])
useEffect(() => { initRustLibrary() }, [initRustLibrary]);
return ( <main className="flex min-h-screen flex-col items-center p-24"> {/* .... */} <div className="mt-32 grid text-center lg:mb-0 lg:w-full lg:max-w-5xl lg:grid-cols-4 lg:text-left"> <div> rust代码计算结果:{addResult} </div> <div style={{ marginTop: '20px' }}> 二分法方式{calculateTime} </div> </div> </main> );}


性能比较

在IM场景下,聊天消息中核心的处理流程在于数据的排序、去重,大量的数据查找会非常耗时,在这里我们通过二分法的方式对Rust和JavaScript两种实现方式的耗时进行一个简单的对比,Rust代码如下:

use chrono::{DateTime, Utc};use rand::Rng;
#[derive()]#[allow(dead_code)]struct Data { content: String, from: String, head: String, msg_id: String, seq: i32, sid: String, topic: String, ts: DateTime<Utc>,}
impl Data { fn new( content: String, from: String, head: String, msg_id: &str, seq: i32, sid: String, topic: String, ts: DateTime<Utc>, ) -> Self { Data { content, from, head, msg_id: msg_id.to_string(), seq, sid, topic, ts, } }}
// 获取原始数据fn get_origin_data(num: i32) -> Vec<Data> { let mut data: Vec<Data> = vec![]; // 存储数据的向量 .... // 创建 num 个数据 data}// 初始化结构体数据fn init_struct_data(num: i32, text: &str) -> Data { let mut rng = rand::thread_rng(); let content = format!("{}_{}", rng.gen_range(1000..=9999), text).to_string(); .... let ts = Utc::now(); Data::new(content, from, head, &msg_id.as_str(), seq, sid, topic, ts)}
// 二分法插入fn binary_insert(data: &mut Vec<Data>, new_data: Data) { let _insert_pos = match data.binary_search_by_key(&new_data.seq, |d| d.seq) { Ok(pos) => { data[pos] = new_data; pos } Err(pos) => { data.insert(pos, new_data); pos } };}#[wasm_bindgen]pub extern "C" fn msg_insert(num: i32) -> String { let mut data: Vec<Data> = get_origin_data(1000); let test_mode = [num]; let start_time = Utc::now().naive_utc().timestamp_micros(); for test_num in 0..test_mode.len() { for num in 0..test_mode[test_num] { let data_list = init_struct_data(num, "test"); binary_insert(&mut data, data_list); } } let duration = Utc::now().naive_utc().timestamp_micros() - start_time; let result = format!("插入{}条数据执行耗时:{}微秒", num, duration); result}


数据对比分析:


可以看到,在数据量不大的场景下,Wasm的耗时是比纯JavaScript长的,这是因为浏览器需要在VM容器中对 Wasm模块进行实例化,这一部分会消耗相当的时间,导致性能不如纯JavaScript的执行。但随着运算规模变大,Wasm的优化越来越明显。这是因为WebAssembly是一种低级别的二进制格式,经过高度优化,并且能够更好地利用系统资源。相比之下,JavaScript是一种解释性语言,性能可能会受到解释器的限制。


总结

在大多数场景下我们都不需要用到WebAssembly。因为V8等JS引擎的优化带来了巨大的性能提升,已经足够让JavaScript应对绝大多数的普通场景了,如果要做进一步优化密集计算任务时使用Web worker也都能解决掉。只有在以上的少数场景下,我们才需要做这种“二次提升”。


WebAssembly虽然有天然的优势,但也有自己的局限性,在使用时我们也需要考虑多方面因素,例如生态、开发成本等等。不过我们依然可以持续关注WebAssembly的发展。


往期回顾


1.实时特征框架的生产实践|得物技术

2.商家下载中心设计演进之路|得物技术

3.增长在流量规则巡检的探索实践|得物技术

4.得物iOS函数调用栈及符号化调优实践|得物技术

5.自研E2E为稳定性保驾护航 | 得物技术


文 / WWQ


关注得物技术,每周一、三更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

扫码添加小助手微信

如有任何疑问,或想要了解更多技术资讯,请添加小助手微信:


跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

WebAssembly 性能提升 应用场景 实践案例
相关文章