什么是wasm組件?
wasm 全稱 WebAssembly,是通過(guò)虛擬機(jī)的方式,可以在服務(wù)端、客戶端如瀏覽器等環(huán)境執(zhí)行的二進(jìn)制程序。它有速度快、效率高、可移植的特點(diǎn)。
對(duì)我們 Web 前端工程最大的好處就是可以在瀏覽器端使用二進(jìn)制程序處理一些計(jì)算量大的處理,使用他比 javascript 快的特點(diǎn)優(yōu)化性能。
目前瀏覽器對(duì)wasm的兼容性如下:
https://img10.360buyimg.com/imagetools/jfs/t1/180904/35/36038/170761/64ded9bdF6f54c383/e85e037cdd4fa1fd.jpg
在移動(dòng)端除了 android 4.4 和 ios 10 下不支持外,其他版本都能提供支持。還需要注意的是 wasm 有可能占用大量?jī)?nèi)存,使用第三方包含 wasm 調(diào)用的組件需要注意內(nèi)存占用防止閃退。
為什么用Rust?
wasm模塊 可以用多種語(yǔ)言來(lái)編譯,包括 C/C++/C#、Rust、JAVA、Go。在這里使用 Rust 是因?yàn)樗袊?yán)格的內(nèi)存管理機(jī)制,從語(yǔ)法上盡量避免內(nèi)存溢出,讓工程師寫(xiě)出更安全的程序。
而且還有配套的工具 wasm-pack,讓使用 Rust 編寫(xiě)的代碼,編譯包裝成 npm 包,讓使用這段程序的其他代碼可以像使用其他公共庫(kù)一樣調(diào)用,不需要額外學(xué)習(xí)成本。
工具安裝
安裝 rustup,它是 Rust 安裝器和版本管理工具。對(duì)于 web 前端來(lái)說(shuō)相當(dāng)于 nvm 這樣的工具。按照 Rust 官網(wǎng)的方法安裝:https://www.rust-lang.org/zh-CN/tools/install同時(shí)也會(huì)安裝 cargo,它是 Rust 的構(gòu)建工具和包管理器。對(duì)于 web 前端來(lái)說(shuō)相當(dāng)于 npm 這樣的工具。
安裝 wasm-pack,他是上文提到的把 Rust 程序編譯包裝成 wasm 組件的工具。同樣按照 wasm-pack 官網(wǎng)的方法安裝:https://rustwasm.github.io/wasm-pack/installer/
使用 wasm 模板使用 wasm-pack 提供的模板可以快速生成 Rust 的 wasm 項(xiàng)目。
cargogenerate--githttps://github.com/rustwasm/wasm-pack-template
輸入希望的項(xiàng)目目錄名稱,將新建目錄并在其中生成項(xiàng)目。
在目錄下我們可以看到幾個(gè)文件,其中一個(gè)是 Cargo.toml ,這個(gè)是 Rust 項(xiàng)目的描述文件,對(duì)于 web 前端來(lái)說(shuō)相當(dāng)于 package.json 文件。
項(xiàng)目目錄下還有一個(gè) src 目錄,里面有 lib.rs 和 utils.rs 兩個(gè)文件,其中 lib.rs 這個(gè)文件就是我們主要的邏輯入口,他引用了 wasm-bindgen 庫(kù)來(lái)輸出暴露給外部調(diào)用的接口,在函數(shù)之前加上#[wasm_bindgen]可以讓外部調(diào)用這個(gè)方法。
編譯項(xiàng)目
本來(lái) Rust 的項(xiàng)目編譯用的是 cargo build 的命令,但是我們這里是希望編譯 wasm 組件,所以用的是 wasm-pack build 命令。
執(zhí)行后會(huì)在項(xiàng)目目錄下的 pkg 目錄下生成編譯后的產(chǎn)品,是一個(gè) npm 包的結(jié)構(gòu)。需要調(diào)用這個(gè)組件的邏輯只需要像其他公共包一樣 import 就可以使用了。
實(shí)戰(zhàn)
以上的就是 wasm-pack 官方的教程,還有其他組件測(cè)試、發(fā)布等的流程先不在這里介紹了。以下用一個(gè)實(shí)際開(kāi)發(fā)中的模塊來(lái)說(shuō)一下開(kāi)發(fā) wasm 組件過(guò)程中遇到的問(wèn)題和解決方法。
背景—
需要使用的 wasm 組件是一個(gè)優(yōu)化3D模型的方法,傳入一個(gè)模型的頂點(diǎn)信息和距離閾值,比較每個(gè)頂點(diǎn)位置之間的距離,如果沒(méi)達(dá)到閾值距離就合并這兩個(gè)頂點(diǎn),以達(dá)到減少頂點(diǎn)的優(yōu)化目的。
原邏輯是使用 javascript 編寫(xiě)的,在模型頂點(diǎn)數(shù)量比較多的時(shí)候執(zhí)行的時(shí)間比較長(zhǎng)。這種大量計(jì)算的情況就很適合使用 wasm 來(lái)處理。
數(shù)據(jù)傳遞—
頂點(diǎn)信息是存儲(chǔ)在一個(gè) Float32Array 的數(shù)組中的,而 wasm 設(shè)計(jì)上除了 int 和 float 類型(對(duì)應(yīng) javascript 就是 number 類型)可以直接傳遞外,其他的類型都通過(guò)地址來(lái)傳遞。這對(duì)我們的程序來(lái)說(shuō)是好消息,因?yàn)轫旤c(diǎn)信息的數(shù)據(jù)非常多,如果以值傳遞,就需要做數(shù)據(jù)復(fù)制,這個(gè)過(guò)程消耗的時(shí)間可能比我們換成 wasm 處理減少的時(shí)間還要多。得益這個(gè)特點(diǎn),我們的入?yún)⒖梢灾苯觽魅搿?/p>
/*---rust----*/ //rust獲取javasctipt數(shù)據(jù) pubfnadd_attribute(&mutself,attribute:&Float32Array,item_size:u32){ self.attributes.push(BufferAttribute{ array:attribute.to_vec(), item_size, }); }
/*---javascript----*/ //javascript傳遞數(shù)據(jù)到rust for(constnameofattributeNames){ constattr=attrArrays[name] bg.add_attribute(attr.array,attr.itemSize) }
而計(jì)算后的結(jié)果,wasm 也提供了返回?cái)?shù)組的指針和數(shù)組長(zhǎng)度的方法,javascript 可以讀取 wasm 的內(nèi)存空間,根據(jù)這兩個(gè)值構(gòu)造新的頂點(diǎn)信息Float32Array。
/*---rust----*/ //返回指定數(shù)據(jù)的內(nèi)存指針位置 pubfnget_attribute_ptr(&self,index:usize)->*constf32{ self.attributes[index].array.as_ptr() } //返回指定數(shù)據(jù)的長(zhǎng)度 pubfnget_attribute_length(&self,index:usize)->usize{ self.attributes[index].array.len() }
/*---javascript----*/ //javascript或取rust內(nèi)存空間中的指定部分,構(gòu)建Float32Array constptr=bg.get_attribute_ptr(i) constlength=bg.get_attribute_length(i) constbuffer=newattr.array.constructor(wasm.getMemory().buffer,ptr,length)
數(shù)據(jù)類型—
合并頂點(diǎn)計(jì)算的邏輯中,有一段是這樣的:每個(gè)頂點(diǎn)的位置、UV等信息,經(jīng)過(guò)給定的精度計(jì)算后,生成一個(gè)特征值,之后比較每個(gè)頂點(diǎn)的特征值,如果是相同的話就表示這兩個(gè)頂點(diǎn)可以合并。
原 javascript 版本的代碼是逐個(gè)信息按順序,加上分隔號(hào),拼成一個(gè)字符串。
Rust 版本的代碼如果也按同樣的方法處理,因?yàn)轫旤c(diǎn)的信息量是不定的,有可能只有位置信息,也有可能有UV、法線、顏色等信息,所以生成的特征值字符串長(zhǎng)度也不確定。
Rust 對(duì)於可變長(zhǎng)度的字符串使用 String 類型,每次對(duì)字符串使用push_str方法增加內(nèi)容。得到的結(jié)果 wasm 版本的執(zhí)行速度跟 javascript 版本相差不大,甚至在某些情況下耗時(shí)還更多,經(jīng)過(guò)逐個(gè)過(guò)程作排查,發(fā)現(xiàn)是在生成特征值和在表中查詢特征值這個(gè)過(guò)程中花費(fèi)的時(shí)間比較多。
根據(jù)程序的意圖,特征值并不一定要是字符串,只需要在不同輸入值的時(shí)候能夠輸出相關(guān)的值就可以,這跟生成 hash 值的需求是一樣的,于是考慮將特征值生成替換成 hash 值計(jì)算。
因?yàn)樵诖鎯?chǔ)特征值的表使用了std::hash_map類型,于是 hash 值也使用了其下的std::DefaultHasher類來(lái)計(jì)算
usestd::DefaultHasher; ... letmuthasher=DefaultHasher::new(); forjin0..self.attributes.len(){ ... letvalue=(attr.array[i*attr.item_sizeasusize+indexasusize] *self.shift_multiplier) .trunc()asi32; hasher.write_i32(value); ... } lethash=hasher.finish();
需要注意的是對(duì)寫(xiě)入不同類型的內(nèi)容,需要調(diào)用不同的方法,頂點(diǎn)信息中的值是正負(fù)值都用,經(jīng)過(guò)精度計(jì)算后取整得到的值類型是i32,所以用write_i32來(lái)寫(xiě)入內(nèi)容。
生成的 hash 值為u64,作為hash_map的key記錄對(duì)應(yīng)頂點(diǎn)的序號(hào)。
替換特征值的類型之后,wasm 版本的耗時(shí)達(dá)到了 javascript 版本的 1/2,基本符合 wasm 設(shè)計(jì)的性能范圍。
適配打包工具—
wasm-pack 工具打包出來(lái)的 npm 包,可以直接在webpack下加載并調(diào)用運(yùn)行。
我們?cè)镜捻?xiàng)目使用 vite 構(gòu)建,vite 對(duì)import wasm 組件策略和 webpack 的不一樣,vite 加載會(huì)返回一個(gè)加載方法,調(diào)用加載方法會(huì)返回一個(gè) Promise,resolve 后才會(huì)返回跟 webpack 加載一樣的 wasm 組件。
我們要對(duì) wasm-pack 生成的產(chǎn)物作一些修改,假設(shè)我們的 wasm 組件命名為 merge_vertice_wasm,生成的主 js 文件應(yīng)該會(huì)命名為merge_vertice_wasm.js,內(nèi)容如下:
import*aswasmfrom'./merge_vertice_wasm_bg.wasm' import{__wbg_set_wasm}aswasm_bgfrom'./merge_vertice_wasm_bg.js' __wbg_set_wasm(wasm); export*from'./merge_vertice_wasm_bg.js'
為兼容 vite 的加載策略,修改成下面的內(nèi)容
import*aswasmfrom'./merge_vertice_wasm_bg.wasm' import*aswasm_bgfrom'./merge_vertice_wasm_bg.js' letmemory if(wasm.default){ wasm.default({ './merge_vertice_wasm_bg.js':wasm_bg, }).then(_wasm=>{ memory=_wasm.memory wasm_bg.__wbg_set_wasm(_wasm) }) }else{ memory=_wasm.memory wasm_bg.__wbg_set_wasm(wasm) } export*from'./merge_vertice_wasm_bg.js' exportfunctiongetMemory(){ returnmemory }
就可以在 webpack 和 vite 下都可以順利加載并運(yùn)行了。
其中增加了getMemory的方法供外部獲取 wasm 組件的內(nèi)存空間。
wasm 調(diào)用 javascript 方法—
當(dāng)我們?cè)谡{(diào)試和測(cè)試性能表現(xiàn)時(shí),需要打印日志,由于我們的 wasm 跑在瀏覽器環(huán)境中,我們需要調(diào)用 javascript 的方法,比如console.log和console.time。
wasm-bindgen 庫(kù)提供了 web-sys 的組件,讓 Rust 可以調(diào)用這些方法。
首先需要在cargo.toml中添加 web-sys 的依賴,并聲明需要用到的特性:
[dependencies] wasm-bindgen="0.2.84" [dependencies.web-sys] version="0.3.64" features=["console"]
這樣在下次編譯的時(shí)候,cargo 就會(huì)自動(dòng)處理這些依賴,將會(huì)下載并構(gòu)建。
然后在我們的 Rust 文件中,加入對(duì) web-sys 的引用:
externcrateweb_sys;
就可以調(diào)用 javascript 的 console 下的方法了:
//調(diào)用console.log web_sys::log_1(&JsValue::from(logContent)); //調(diào)用console.time(label) web_sys::time_with_label(label); //調(diào)用console.timeEnd(label) web_sys::time_end_with_label(label);
原 javascript 版本優(yōu)化模型耗時(shí):
https://img14.360buyimg.com/imagetools/jfs/t1/109410/21/37527/8537/64dedd1cFe4c8c5c4/596fc2d36cc9fe5c.jpg
wasm 版本優(yōu)化模型耗時(shí):
https://img12.360buyimg.com/imagetools/jfs/t1/188745/32/36809/10529/64dedd1cF49a8b5cc/8dea820d278ad577.jpg
總結(jié)
以上為根據(jù)官網(wǎng)文檔把模型合并頂點(diǎn)優(yōu)化方法遷移為 wasm 版本的開(kāi)發(fā)經(jīng)歷,從安裝工具到發(fā)布、調(diào)試的整個(gè)過(guò)程。
中間因?yàn)閷?duì) Rust 數(shù)據(jù)類型的不熟悉和對(duì)不同前端構(gòu)建工具對(duì) wasm 組件處理的不同不夠清晰,在開(kāi)發(fā)過(guò)程中遇到的問(wèn)題和解決方法。
Rust 版本的代碼邏輯基本上是從 javascript 版本翻譯過(guò)來(lái)的,其中應(yīng)該還有在 Rust 環(huán)境下的優(yōu)化手段,將在之后的學(xué)習(xí)中繼續(xù)迭代。
審核編輯:湯梓紅
-
Web
+關(guān)注
關(guān)注
2文章
1263瀏覽量
69470 -
C++
+關(guān)注
關(guān)注
22文章
2108瀏覽量
73651 -
Rust
+關(guān)注
關(guān)注
1文章
228瀏覽量
6610
原文標(biāo)題:給Web前端工程師看的用Rust開(kāi)發(fā)wasm組件實(shí)戰(zhàn)
文章出處:【微信號(hào):OSC開(kāi)源社區(qū),微信公眾號(hào):OSC開(kāi)源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論