基于IM场景下的Wasm初探:提升Web应用性能|得物技术

news2024/11/26 16:34:36

一、何为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是解释型语言,相比于编译型语言需要在运行时转换,所以解释型语言的执行速度要慢于编译型语言。

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

image.png

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

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

三、优势和限制

优势:

  • 性能优异:相比JavaScript代码,Wasm使用节省内存,快速加载和解释的二进制代码,具备更快执行速度,它是直接在底层虚拟机中运行的。这使得Web应用程序可以更高效地处理复杂的计算任务,例如图形渲染、物理模拟等。

  • 跨平台兼容:Wasm可以在几乎所有现代浏览器中运行,兼容性可参考caniuse,无论是桌面还是移动设备。这意味着开发者可以使用各种编程语言来编写Web应用程序,而不仅仅局限于JavaScript。

  • 安全性:Wasm运行在沙箱环境中,提供了良好的安全性。使用了一系列安全措施,如内存隔离和沙箱限制,以防止恶意代码对系统的攻击。

  • 模块化:Wasm模块可以作为独立的组件进行开发和部署,开发者可以更好地管理和维护代码库。模块化的设计也为将来的性能优化和增量更新提供了便利。

局限性:

  • 生态系统不够完善:尽管Wasm已经成为Web开发中的关键技术之一,但生态系统仍然不够完善。Wasm的工具、框架和库的数量远不如JavaScript。

  • 开发门槛较高:Wasm的开发门槛相对较高。Wasm需要使用一种新的语言来编写,如C或C++等。这使得学习和使用Wasm的成本相对较高。尤其是在内存管理等方面会增加开发的复杂性。

  • 与JavaScript集成问题:Wasm与JavaScript之间的集成问题是一个挑战。开发人员需要解决如何在Web应用程序中同时使用Wasm和JavaScript的问题。

  • 兼容性问题:虽然现代浏览器已经开始支持Wasm,但是在一些老旧的浏览器中可能存在兼容性问题,需要开发者进行额外的处理来确保代码的兼容性。

四、Wasm工作原理

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

image.png

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

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

image.png

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

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

五、应用场景

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

  • 高性能计算:如果应用程序需要进行大量的数值计算、图像处理或者复杂的算法运算,可以将这部分代码编译成Wasm模块,以提高计算性能。

  • 游戏开发:Wasm可以用于创建高性能的HTML5游戏,通过将游戏逻辑编译成Wasm模块,可以实现更流畅的游戏体验。

  • 跨平台应用:使用Wasm可以实现跨平台的应用程序,无论是桌面还是移动设备,用户都可以通过浏览器来访问和使用。

  • 移植现有代码:如果已经有用其他编程语言编写的代码,可以通过将其编译成Wasm模块,将其集成到现有的Web应用程序中,而无需重写整个应用程序。

六、产品案例

  • 设计工具Figma-Wasm文件大小为27.7M

image.png

  • Google Earth-Wasm文件总计大小为192.M

  • 支持各大浏览器的3D地图,而且运行流畅

image.png

  • B站-视频处理和播放也有使用Wasm,Wasm文件大小为344kb

image.png

  • 跨平台的OpenGL图形引擎Magnum-Wasm文件大小为844kb
    image.png

七、实践案例

这里我们通过使用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会自动下载依赖。

  • crate-type = [“cdylib”],表示编译时候使用C标准的动态库。

  • #[wasm_bindgen]是一个属性宏,来自于wasm_bindgen这个crate,是一个简化Rust WASM与JS之间交互的库。

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = { version = "0.2.89", features = [] }

编写代码

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

二进制包:

  • main.rs是二进制项目的入口

  • 二进制项目可直接执行

  • 一个项目中二进制包可以有多个,所以在Cargo.toml中通过双方括号标识 [[bin]]

库包:

  • lib.rs是库包的入口

  • 库项目不可直接执行,通常用来作为一个模块被其他项目引用

  • 一个项目中库包仅有1个,在Cargo.toml中通过单方括号标识 [lib]

因为我们这里希望将 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方式可以指定构建的产物,如截图所示:

image.png

编译过程效果:

编译完成后,我们会发现根目录下多了一个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文件,对模块的使用者非常友好。如下:

image.png

在前端项目中引入使用

'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>
  );
}

image.png

性能比较

在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
}

数据对比分析:

image.png

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

八、总结

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

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

*文/ WWQ

本文属得物技术原创,更多精彩文章请看:得物技术

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2234992.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【JAVA毕业设计】基于Vue和SpringBoot的墙绘产品展示交易平台

本文项目编号 T 049 &#xff0c;文末自助获取源码 \color{red}{T049&#xff0c;文末自助获取源码} T049&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析 六、核心代码6.1 查…

在全域数据整合过程中,如何确保数据的一致性和准确性

在全域数据整合过程中&#xff0c;确保数据的一致性和准确性是至关重要的&#xff0c;这不仅关系到数据分析结果的可靠性&#xff0c;还直接影响到企业决策的科学性和有效性。Aloudata AIR 逻辑数据编织平台通过数据虚拟化技术&#xff0c;为这一过程提供了强有力的支持。以下是…

w024基于SpringBoot的企业客户管理系统的设计与实现

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以查看文章末尾⬇️联系方式获取&#xff0c;记得注明来意哦~&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0…

element-plus快速实现table组件表头吸顶和滚动条吸底

使用devecoui 组件快速实现 element-plus table 组件&#xff0c;表头吸顶和滚动条吸底&#xff0c;且完美实现固定表头的吸顶效果&#xff0c;同时devecoui组件还可以实现&#xff0c;查询列表的快速开发&#xff0c;里面集成了更多的功能。详细功能请前往&#xff1a;添加链接…

C++STL容器详解——list

目录 一.list 1.list的介绍 2.为什么会有list? 二.list的常见接口 1.list的构造函数 2.list的遍历 3.迭代器类型 4.list的头插头删和尾插尾删 5.list任意位置的插入和删除 6.list的sort()及reverse() 7.迭代器失效 三.整体代码 一.list 1.list的介绍 list的文档说…

服务器数据恢复—EVA存储故障导致上层应用不可用的数据恢复案例

服务器存储数据恢复环境&#xff1a; 一台EVA某型号控制器EVA扩展柜FC磁盘。 服务器存储故障&检测&#xff1a; 磁盘故障导致该EVA存储中LUN不可用&#xff0c;导致上层应用无法正常使用。 服务器存储数据恢复过程&#xff1a; 1、将所有磁盘做好标记后从扩展柜中取出。硬…

hf_transformers

强者自定义&#xff0c;弱者用默认&#xff0c;傻逼不看说明书 1. 2.在 model.generate()里填参数&#xff0c;默认为20个新token generated_ids model.generate(**model_inputs, max_new_tokens50) 3. 默认情况下&#xff0c;除非在GenerationConfig文件中指定&#xff0…

Sigrity Power SI 3D-EM Inductance Extraction模式如何进行电感的提取操作指导(一)

Sigrity Power SI 3D-EM Inductance Extraction模式如何进行电感的提取操作指导(一) Sigrity Power SI使用3D-EM Inductance Extraction模式可以进行电感的提取,以下图为例 2D 视图 <

学习记录:js算法(八十七):单词搜索

文章目录 单词搜索思路一思路二 单词搜索 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 单词必须按照字母顺序&#xff0c;通过相邻的单元格内的字母构成&#xff0c;其…

应急救援无人车:用科技守护安全!

一、核心功能 快速进入危险区域&#xff1a; 救援无人车能够迅速进入地震、火灾、洪水等自然灾害或重大事故的现场&#xff0c;这些区域往往对人类救援人员构成极大威胁。 通过自主导航和环境感知技术&#xff0c;无人车能够避开危险区域&#xff0c;确保自身安全的同时&…

辩论赛——动态IP与静态IP的巅峰对决

尊敬的各位观众&#xff1a; 大家好&#xff01;欢迎来到今天的演说舞台&#xff0c;我是主持人小蝌蚪。今天&#xff0c;我们将见证一场精彩绝伦的辩论&#xff0c;辩论的双方是动态IP和静态IP。他们将围绕各自的优缺点展开激烈的辩论&#xff0c;为我们揭示代理IP世界中的奥…

红米k70怎么设置「短信通知」在锁屏时隐藏内容,不锁屏时不隐藏内容

红米 K70 设置短信通知在锁屏时隐藏内容、不锁屏时不隐藏内容&#xff0c;可以按照以下步骤进行操作&#xff1a; 打开手机设置&#xff1a;在主屏幕上找到并点击 “设置” 图标&#xff0c;进入手机设置页面。进入通知与控制中心&#xff1a;在设置页面中&#xff0c;找到并点…

【计算机网络】零碎知识点(易忘 / 易错)总结回顾

一、计算机网络的发展背景 1、网络的定义 网络是指将多个计算机或设备通过通信线路、传输协议和网络设备连接起来&#xff0c;形成一个相互通信和共享资源的系统。 2、局域网 LAN 相对于广域网 WAN 而言&#xff0c;局域网 LAN 主要是指在相对较小的范围内的计算机互联网络 …

Python 在PDF中绘制形状(线条、矩形、椭圆形等)

在PDF中绘制图形可以增强文档的视觉效果。通过添加不同类型的形状&#xff0c;如实线、虚线、矩形、圆形等&#xff0c;可以使文档更加生动有趣&#xff0c;提高读者的阅读兴趣。这对于制作报告、演示文稿或是教材特别有用。本文将通过以下几个示例介绍如何使用Python 在PDF中绘…

三菱MR-J4伺服绝对位置检测系统

发生[AL.25 绝对位置丢失]或[AL.E3 绝对位置计数器警告]时&#xff0c;必须再次进行原点设定。否则可能会因此发生预料之外的动作。 概要 常规运行时&#xff0c;编码器由检测1转内位置的编码器和检测转数的旋转累计计数器构成。 绝对位置检测系统与伺服系统控制器电源…

程序员行业会因此受到什么冲击?

床铺再次当选&#xff0c;会对两家关系产生深远影响。在此篇博客中&#xff0c;我们将探讨床铺的政策对我们外贸、就业、留学以及特别是互联网产业和我们程序员职业的潜在影响。 关系趋紧&#xff1a;摩擦可能会更多 床铺在其任期期间对我们施加了诸多贸易税&#xff0c;采取…

Edge浏览器打开PDF无法显示电子签章

Edge浏览器打开PDF无法显示电子签章 直接说处理方式 直接说处理方式 浏览器地址栏&#xff0c;输入 edge://flags/搜索&#xff1a;pdf禁用&#xff1a;New PDF Viewer效果如下

02- 模块化编程-006 ADC0808数码显示对比

1、ADC0808 芯片介绍 ADC0808是一款集成的CMOS设备&#xff0c;包含8位模拟至数字转换器、8通道多路复用器和与微处理器兼容的控制逻辑。8位A/D转换器采用逐次逼近作为转换技术。转换器特点包括高阻抗斩波稳定比较器、256R电压分压器、模拟开关树和逐次逼近寄存器。8通道多路复…

计算机体系结构之多级缓存、缓存miss及缓存hit(二)

前面章节《计算机体系结构之缓存机制原理及其应用&#xff08;一&#xff09;》讲了关于缓存机制的原理及其应用&#xff0c;其中提出了多级缓存、缓存miss以及缓存hit的疑问。故&#xff0c;本章将进行展开讲解&#xff0c; 多级缓存、缓存miss以及缓存hit存在的意义是为了保持…

scala set训练

Set实训内容&#xff1a; 1.创建一个可变Set&#xff0c;用于存储图书馆中的书籍信息&#xff08;假设书籍信息用字符串表示&#xff09;&#xff0c;初始化为包含几本你喜欢的书籍 2.添加两本新的书籍到图书馆集合中&#xff0c;使用操作符 3.删除一本图书馆集合中的书籍&…