WebAssembly学习记录

1.WebAssembly

1.1 指令集

概念:二进制编码集合。

依据计算机组成原理和计算机概论,指令集是一组二进制编码。

作用:控制硬件。

这些二进制指令直接作用于硬件电路,控制硬件完成指定操作。

  • 例如:控制数据进入某个寄存器,控制数据做位运算。

分类: 按CPU分类。

不同架构的CPU有不同的指令集。

  • 例如:x86和arm架构CPU指令集不同。

1.2 WebAssembly

概念:低级静态程序语言。

WebAssembly可以看做是针对于浏览器的一组指令集。它是低级程序语言。

  • 例如:它和汇编语言类似,只不过不同CPU的汇编语言不同,但是不同浏览器的WebAssembly都相同。

作用:在浏览器中运行。

除了JavaScript外另一种可以直接在浏览器中运行的语言。

  • 例如:因为WebAssembly可以在浏览器中运行,因此C/C++,Rust等语言选择把它们编译成WebAssembly在浏览器运行。

特点:效率高。

不需要通过高级程序员的解释器或编译器,是编译产物,免去这个耗时操作。

  • 例如:高级程序语言的一系列操作可以被汇编级语言优化成几次简单操作,例如优化后简单将数据在寄存器间移动就可能完成多个语句表示的操作。

2.优化密集计算型业务

2.1 编译流程概述

流程背景

编译C/C++指定函数到wasm文件,为JavaScript提供计算密集型任务的高效解决方案。

流程概述

按照emscripten官方文档安装编译器并对C/C++打包。

注:也可以编译整个C/C++文件到wasm,本流程只导出函数

  • 激活环境: 运行 emsdk_env.batemsdk_env.sh激活编译器环境

  • 编译: 运行 emcc test1.cpp -s EXPORTED_FUNCTIONS=_fib,_add -O3 -o fib-emcc.wasm编译成wasm文件

命令说明

注:官网上提到过导出cwrap,ccall来给JavaScript调用导出的C/C++函数。但是安装最新版emscripten发现没用。

  • -s EXPORTED_FUNCTIONS= 指明导出的C/C++函数,函数名前需要加下划线
  • -O 指明编译优化方案,和g++中 -O指令含义相同

2.2 编译流程详解

2.2.1 C/C++部分

extern "C"连接指示符:

按C语言规则来处理函数名称。Emscripten要求导出C++函数时必须有该操作。

main函数:

Emscripten要求导出C/C++函数时,即使不导出main函数也要必须写main函数。

开辟内存和写入内存函数:

JavaScript传入引用类型给Wasm模块时直接传入会失败。因为C++中引用类型需要指明指针地址,如果JavaScript需要传入引用类型,需要在Wasm模块中开辟一块空间写入传入的参数,这样C++才能访问到。如果传入普通类型不需要考虑开辟空间。

#include<algorithm>extern "C" {// 将导出给JavaScript:a+b函数int add(int a, int b) {return a + b;}// 将导出给JavaScript:开辟内存空间函数int* createIntArray(int length) {return new int[length];}// 将导出给JavaScript:写入内存空间函数void writeToArray(int* arr, int index, int value) {arr[index] = value;} // 将导出给JavaScript:快排函数int* qSort(int* arr, int length) {		std::sort(arr, arr + length);return arr;}// 将导出给JavaScript:斐波那契数列函数int fib (int n) {if (n <= 0) return 0;if (n <= 2) return 1;return fib(n - 2) + fib(n - 1);}
}int main () {}
2.2.2 JavaScript部分

读取WebAssembly文件:

读取WebAssembly文件方法有很多,经过若干版本迭代,使用MDN最新推荐的方法 WebAssembly.instantiateStreaming(fetch(fileName), importObject)读取

传入C/C++模块初始值:

类似node运行时可以传参 node build.js --env=development一样,C/C++编译后的wasm模块也需要传参。传参的标准可以参考WASI(专门为WebAssembly设计的)规范。传参通过读取wasm模块的第二个参数传递,是否传参由wasm模块决定。

由下图圈出部分可知需要给wasm模块传递什么样的参数。

注:直接打开wasm文件打不开,都是二进制。可以通过浏览器查看对应的浏览器层面的汇编语言。

注:下面的传参很简单。如果在C/C++引入了更多的模块,例如 #incldue<iostream>需要传入更多初始参数。

在这里插入图片描述

传递引用到wasm模块:

下述代码在调用wasm的 qSort时不能直接把一个JavaScript数组传递过去,这样会导致C/C++的快排函数读取失败。传递引用时需要有C/C++编程思想:

  • 传递指针: 首先肯定要传递指针,指针的值实际是地址,所以需要传数值。
  • 申请空间: 需要在wasm模块中为函数形参申请内存空间,并将需要排序的数组写入。

读取引用从wasm模块:

读取引用类型和传递引用类型类似,都要以指针为中心。

  • 读取指针: qSort函数返回指针,其实就是数值,需要我们从wasm模块的内存中读取对应地址内容。
  • 转换为JavaScript对象: 转换为JavaScript对象一般利用 Buffer类实现,下面文件接收 int数组,因此使用 Int32Array类转化为JavaScript对象。
<html><head><meta charset="utf-8" /></head><body><script>// 初始化随机数据const handleInitData = (length) => {return Array.from({ length }).map(() =>Math.floor(Math.random() * length));};// 加载WebAssembly文件const handleUseWebAssembly = (fileName) => {// C++运行时传入的参数(具体传入什么变量需要等wasm生成后再分析。)const importObject = {wasi_snapshot_preview1: {proc_exit: function (code) {console.log(`Process exited with code ${code}`);},},};// 利用fetch加载wasm文件return WebAssembly.instantiateStreaming(fetch(fileName), importObject);};// 把数据传给wasm模块const handlePostData = (result, initData) => {// 获取C++提供的函数const {// 在C++所在的wasm中申请数组内存createIntArray,// 在C++所在的wasm中向某个内存地址写入数据writeToArray,} = result?.instance?.exports;const length = initData.length;// 在wasm申请一片内存空间,用于存储数组const arrPointer = createIntArray(length);// 向刚刚申请的内存空间中写入数据Array.from({ length }).forEach((_, index) =>writeToArray(arrPointer, index, initData[index]));return arrPointer;};// 排序const handleSort = (result, initData, dataPointer) => {const length = initData.length;const startJS = performance.now();// sort默认转成字符串排序,需要转回数值再排序initData.sort((a, b) => Number(a) - Number(b));const endJS = performance.now();console.log(`JS排序${length}条数据用时${(endJS - startJS) / 1000}`);const { qSort } = result?.instance?.exports;const startC = performance.now();const newPointer = qSort(dataPointer, length);const endC = performance.now();console.log(`C++排序${length}条数据用时${(endC - startC) / 1000}`);return newPointer;};// 输出排序结果const handlePrint = (result, length, pointer, jsResult) => {const data = new Int32Array(// Buffer对象,表示获取WebAssembly文件使用的内存空间result.instance.exports.memory.buffer,pointer,length);const cResult = Array.from(data);console.log("JavaScript排序结果是", jsResult);console.log("C++排序结果是", cResult);};(async () => {// 初始化10万条随机数用于排序const length = 1e6;// 要导入的WebAssembly文件const fileName = "./test-emcc.wasm";// 初始化数据const initData = handleInitData(length);// 读取WebAssembly文件const result = await handleUseWebAssembly(fileName);// 把初始化数据传给WebAssembly文件const dataPointer = handlePostData(result, initData);// 排序const newPointer = handleSort(result, initData, dataPointer);// 输出排序结果handlePrint(result, initData.length, newPointer, initData);})();</script></body>
</html>
2.2.3 Web服务器部分

上面直接引入wasm文件会有同源限制问题。下面用原生node实现一个简单的web服务器解决该问题。

let fs = require("fs");
function read(path, res) {
fs.readFile(path, function (err, data) {if (err) console.log(err);else if (path.slice(-2, path.length) === "js") {res.writeHead(200, {"Content-Type": "text/javascript",});res.write(data);res.end();} else if (path.slice(-4, path.length) === "html") {res.writeHead(200, {"Content-Type": "text/html",});res.write(data);res.end();} else if (path.slice(-4, path.length) === "wasm") {res.writeHead(200, {"Content-Type": "application/wasm",});res.write(data);res.end();}
});
}
require("http")
.createServer(function (req, res) {if (req.url === "/favicon.ico") res.end();else read("." + req.url, res);
})
.listen(3000, function (err) {if (err) console.log(err);else console.log("运行成功");
});

2.3 计算结果

实验结果阐述

输出结果表示wasm模块执行效率要比JavaScript效率高了一倍以上,但是这只是一个简单的使用。网上有其它实验表示平均基于C/C++的wasm模块的效率要比JavaScript高30%~50%,但wasm相比C/C++直接编译后再生成exe文件在windows上运行效率低30%。

在这里插入图片描述

实验结果概述

运行速度:JavaScript > 基于C/C++的wasm模块 > 原生环境的C/C++

2.4 特点分析

效率高

虽然没有让性能成倍提升,但是提升30%~50%的性能还可以让人接受。

编译过程参考少

C/C++编译成wasm时发现Emscripten的文档很老,并且网上参考博客也很古老,参考内容不多,出错不好排查。

需要前端直面指针和内存

相互交换引用类型时,使用JavaScript不得不直面指针和内存操作。和JavaScript设计初衷不符。

3.优化Tensorflow

3.1 优化流程概述

流程背景

为了缓解服务器压力,需要将某些模块的人脸识别功能迁移到前端完成。目前使用了基于tensorflow的 face-api相关库和模型实现,考虑使用多线程和wasm做进一步优化。

流程概述

还没有分析和实现完,暂时用 face-api的测试仓库举例。

注意事项

  • 跨环境对比: face-api中只提供了node环境的wasm模块,对于浏览器环境没有给出wasm模块。因此只用node下的wasm模块和浏览器的普通模块做对比。
  • 不在node环境下对比: 不用node环境作对比是因为需要安装的包太麻烦,里面的tensorflow并不是全部由JavaScript实现,还需要安装精确版本的python和C/C++的编译器。
  • 对比存在差异: face-api提供的node下的wasm版本并不支持使用GPU加速,但是浏览器环境下支持自动使用GPU加速。因此对比时会考虑到GPU加速带来的影响。

3.2 优化流程详解

3.2.1 浏览器部分

基于demo中的index.js进行修改(下面只是截取了修改的部分)。

注:计算时间时考虑YOLO目标检测和face-api人脸识别两个神经网络的时间。

for (const img of samples) {document.body.appendChild(document.createElement("br"));const canvas = await image(img);try {const start = performance.now();const dataTinyYolo = await faceapi.detectAllFaces(canvas, optionsTinyFace).withFaceLandmarks().withFaceExpressions().withFaceDescriptors().withAgeAndGender();const dataSSDMobileNet = await faceapi.detectAllFaces(canvas, optionsSSDMobileNet).withFaceLandmarks().withFaceExpressions().withFaceDescriptors().withAgeAndGender();const end = performance.now();const time = (end - start) / 1000;timeList.push(time);console.log(`图片${timeList.length}耗时${time}`);} catch (err) {log(`Image: ${img} Error during processing ${str(err)}`);}
}console.log(`平均耗时${timeList.reduce((sum, time) => time + sum, 0) / timeList.length}`
);
3.2.2 Node部分

基于demo中的node-wasm.js进行修改。修改内容较多,下述代码给出了详细的注释。

注:计算时间时由于没有YOLO目标检测,这里只计算face-api识别人脸的时间。

const fs = require("fs");
const image = require("@canvas/image");
const tf = require("@tensorflow/tfjs");
const wasm = require("@tensorflow/tfjs-backend-wasm");
const faceapi = require("../dist/face-api.node-wasm.js");async function readImage(imageFile) {
const buffer = fs.readFileSync(imageFile);
const canvas = await image.imageFromBuffer(buffer);
const imageData = image.getImageData(canvas);// 图片转化成tensor向量
const tensor = tf.tidy(() => {const data = tf.tensor(Array.from(imageData?.data || []),[canvas.height, canvas.width, 4],"int32");const channels = tf.split(data, 4, 2);const rgb = tf.stack([channels[0], channels[1], channels[2]], 2);const squeeze = tf.squeeze(rgb);return squeeze;
});
return tensor;
}async function main() {
// 加载wasm形式的tensorflow
wasm.setWasmPaths(// 国内jsdelivr镜像"https://jsd.cdn.zzko.cn/npm/@tensorflow/tfjs-backend-wasm/dist/",true
);
await tf.setBackend("wasm");
await tf.ready();console.log(`FaceAPI版本 ${faceapi.version}`);
console.log(`TensorFlow版本 ${tf.version_core}`);
console.log(`TensorFlow模式 ${faceapi.tf.getBackend()}`);// 加载SSD Mobilenet V1神经网络判断所有面部
await faceapi.nets.ssdMobilenetv1.loadFromDisk("model");
// 加载68点阵判断面部神经网络
await faceapi.nets.faceLandmark68Net.loadFromDisk("model");
// 加载年龄和性别神经网络
await faceapi.nets.ageGenderNet.loadFromDisk("model");
// 加载人的脸判断神经网络
await faceapi.nets.faceRecognitionNet.loadFromDisk("model");
// 加载标签神经网络
await faceapi.nets.faceExpressionNet.loadFromDisk("model");// 配置第一个网络SSD神经网络网络的参数(其它神经网络用默认参数)
const options = new faceapi.SsdMobilenetv1Options({minConfidence: 0.1,maxResults: 10,
});// 待检测图片列表
const ImageList = Array.from({ length: 6 });
const getImagePath = (index) => `demo/sample${index}.jpg`;// 依次检测图片(测试发现下面faceapi并行调用会阻塞)
const detectList = ImageList.reduce(async (previousPromise, _, index) => {const timeList = await previousPromise;const tensor = await readImage(getImagePath(index + 1));const start = performance.now();// 调用神经网络进行识别await faceapi.detectAllFaces(tensor, options).withFaceLandmarks().withFaceExpressions().withFaceDescriptors().withAgeAndGender();const end = performance.now();const elapsedTime = (end - start) / 1000;timeList.push(elapsedTime);console.log(`图片${index + 1}耗时${elapsedTime}`);// 销毁向量tf.dispose(tensor);return Promise.resolve(timeList);
}, Promise.resolve([]));detectList.then((list) => {const averageTime = list.reduce((sum, time) => sum + time, 0) / list.length;console.log(`平均时间是${averageTime}`);
});
}main();

3.3 优化结果

结论

不使用GPU加速的tensorflow的wasm版本的速度逼近使用GPU加速的tensorflow的普通JavaScript版本。可以看出wasm还是有明显加速效果的。

浏览器部分

使用GPU加速tensorflow。去除第一个极端例子后,最终平均时间在1秒以内,集成显卡使用率有明显波动。

在这里插入图片描述

Node部分

使用wasm加速tensorflow。最终平均时间是1秒出头,两个GPU的使用率没有明显波动

在这里插入图片描述

4.优化多线程

4.1 优化举例

哔哩哔哩的投稿页面可以看到有两个运用wasm的地方,第一个是直接提供计算密集型的操作函数。第二个是工作线程,可以看出是在tensorflow的工作线程中又引入了wasm提速,主要是用于快速生成AI视频封面。

在这里插入图片描述

4.2 优化线程池举例

例子后续再写

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

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

相关文章

【unocss】自用

unocss中文官网1 不知道简写的可以在这里查 第一步 npm install -D unocss第二步 // vite.config.ts import UnoCSS from unocss/vite import { defineConfig } from viteexport default defineConfig({plugins: [UnoCSS()] })// main.ts import virtual:uno.css第三步 在…

基于Springboot的在线博客网站

基于SpringbootVue的在线博客网站的设计与实现 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringbootMybatis工具&#xff1a;IDEA、Maven、Navicat 系统展示 用户登录 首页 博客标签 博客分类 博客列表 图库相册 后台登录 后台首页 用户管理 博客标…

Android 设置头像 - 裁剪及圆形头像

书接上文 Android 设置头像 - 相册拍照&#xff0c;通过相册和照片的设置就可以获取到需要的头像信息&#xff0c;但是在通常情况下&#xff0c;我们还想要实现针对头像的裁剪功能和圆形头像功能。 先上截图&#xff1a; 图像裁剪 通常裁剪可以分为程序自动裁剪和用户选择裁剪…

基于SpringBoot实现各省距离Excel导出实战

目录 前言 一、列表及图表信息展示 1、数据过滤调整 2、信息列表及图表展示 3、Excel写入 二、界面可视化 1、Echarts图表和列表展示 2、城市详情和下载功能设计 三、成果展示 1、图表展示 2、部分城市数据分析 总结 前言 今天是五一黄金周假期第二天&#xff0c;不知…

电脑自带dll修复在哪里,使用dll修复工具解决dll问题

在我们日常与电脑相伴的工作与学习过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中最常见的就是“无法找到.dll”或“找不到.dll文件”。这种情况通常是由于dll文件丢失或损坏导致的。dll文件是动态链接库文件&#xff0c;它包含了许多程序运行所需的函数和资源…

使用 BurpSuite 基于 Token 机制实施暴力破解

前言 Token是一种用于身份验证和授权的令牌&#xff0c;通常由服务器生成并发送给客户端&#xff0c;客户端在后续的请求中携带该令牌来进行身份验证和授权操作。Token的使用可以增强应用程序的安全性&#xff0c;避免了直接传递敏感凭证&#xff08;如用户名和密码&#xff0…

Golang | Leetcode Golang题解之第61题旋转链表

题目&#xff1a; 题解&#xff1a; func rotateRight(head *ListNode, k int) *ListNode {if k 0 || head nil || head.Next nil {return head}n : 1iter : headfor iter.Next ! nil {iter iter.Nextn}add : n - k%nif add n {return head}iter.Next headfor add > …

golang判断通道chan是否关闭的2种方式

chan通道在go语言的办法编程中使用频繁&#xff0c;我们可以通过以下2种方式来判断channel通道是否已经关闭&#xff0c;1是使用 for range循环&#xff0c;另外是通过 for循环中if 简短语句的 逗号 ok 模式来判断。 示例代码如下&#xff1a; //方式1 通过for range形式判断…

进销存单机版和excel进销存那个好用

进销存单机版和EXCEL进销存哪个好用&#xff1f;单机版是安装在单台电脑上使用的&#xff0c;它不能像网络版一样可以多台电脑同时共享数据&#xff0c;所以进销存单机版有一个优势就是不需要连接网络也可以使用。 进销存单机版 进销存软件单机版是经过开发人员设计好的一种信…

es环境安装及php对接使用

Elasticsearch Elasticsearch 是一个分布式、高扩展、高实时的搜索与数据分析引擎。它提供了一个分布式多用户能力的全文搜索引擎&#xff0c;基于RESTful web接口。Elasticsearch是用Java语言开发的&#xff0c;并作为Apache许可条款下的开放源码发布&#xff0c;是一种流行的…

postman一直转圈圈,无法启动

解决 地址栏输入%appdata%进入此目录&#xff0c;删除%appdata%目录下的postman文件可以解决问题。

贪心算法 Greedy Algorithm

1) 贪心例子 称之为贪心算法或贪婪算法&#xff0c;核心思想是 将寻找最优解的问题分为若干个步骤 每一步骤都采用贪心原则&#xff0c;选取当前最优解 因为没有考虑所有可能&#xff0c;局部最优的堆叠不一定让最终解最优 v2已经不会更新v3因为v3更新过了 贪心算法是一种在…

Ps 滤镜:智能锐化

Ps菜单&#xff1a;滤镜/锐化/智能锐化 Filter/Sharpen/Smart Sharpen 智能锐化 Smart Sharpen滤镜可以用来提高图像的视觉清晰度和边缘细节&#xff0c;同时最大限度地减少常见的锐化问题如噪点和光晕等。 “智能锐化”滤镜通过自适应算法分析图像内容&#xff0c;针对不同的细…

省级财政收入、支出、第一、二、三产业增加值、工业增加值、金融业增加值占GDP比重数据(1978-2022年)

01、数据介绍 财政收支作为国家治理的基础&#xff0c;越来越受到社会各界的关注。同时&#xff0c;产业结构的优化与升级也是中国经济持续增长的关键因素。本数据对中国省级财政收入、支出占GDP的比重以及第一、二、三产业的增加值占GDP的比重和工业增加值占GDP的比重、金融业…

Pandas入门篇(二)-------Dataframe篇5(进阶)(Dataframe的时间序列Dataframe最终篇!!)(机器学习前置技术栈)

目录 概述一、pandas的日期类型&#xff08;一&#xff09;datetime64类型的特点&#xff08;二&#xff09; 时间序列的创建1.从字符串创建datetime64类型2. 整数&#xff08;Unix时间戳&#xff09;创建datetime64类型3.导入数据时直接转换 &#xff08;三&#xff09;dateti…

打印机-STM32版本 硬件部分

最终PCB EDA工程: 一、确定芯片型号 根据项目需求&#xff0c;梳理需要用到的功能&#xff0c; 电量检测&#xff1a;ADC 按键&#xff1a;IO input外部中断 LED&#xff1a;IO output 温度检测&#xff1a;ADC 电机控制&#xff1a;IO output 打印通讯&#xff1a;SPI …

C++string类使用大全

目录 温馨提示&#xff1a;这篇文章有约两万字 什么是string类&#xff1f; 一. 定义和初始化string对象 1.string的构造函数的形式&#xff1a; 2.拷贝赋值运算符 3.assign函数 二.string对象上的操作 1.读写string对象 2.读取未知数量的string对象 3.使用getline …

windows ubuntu sed,awk,grep篇:10.awk 变量的操作符

目录 62. 变量 64. 算术操作符 65. 字符串操作符 66. 赋值操作符 67. 比较操作符 68. 正则表达式操作符 62. 变量 Awk 变量以字母开头&#xff0c;后续字符可以是数字、字母、或下划线。关键字不能用作 awk 变量。 不像其他编程语言&#xff0c; awk 变量可以直接使…

实习面试之算法准备:数学题

目录 1 技巧2 例题2.1 Nim 游戏2.2 石子游戏2.3 灯泡开关 1 技巧 稍加思考&#xff0c;找到规律 2 例题 2.1 Nim 游戏 你和你的朋友&#xff0c;两个人一起玩 Nim 游戏&#xff1a; 桌子上有一堆石头。 你们轮流进行自己的回合&#xff0c; 你作为先手 。 每一回合&#xf…

SpringBoot 打包所有依赖

SpringBoot 项目打包的时候可以通过插件 spring-boot-maven-plugin 来 repackage 项目&#xff0c;使得打的包中包含所有依赖&#xff0c;可以直接运行。例如&#xff1a; <plugins><plugin><groupId>org.springframework.boot</groupId><artifact…