iT邦幫忙

2025 iThome 鐵人賽

DAY 16
0
Rust

把前端加速到天花板:Rust+WASM 即插即用外掛系列 第 16

Day 16|把 memory bound 的坑填起來

  • 分享至 

  • xImage
  •  

做這個套件,算子本身通常是 O(wh) 的線性時間;真正拖慢的東西之前有發現不是演算法,而是搬來搬去的過程,在 JS 與 Wasm 兩個記憶體世界來回拷貝像素。Day 6 的基準你已經看過同一條管線,只要少一次跨邊界拷貝,整體延遲就會明顯下降。所以 Day 16 的重點不是再把卷積寫快 3%,而是把資料流「關在」Wasm 裡,讓像素從載入到產出都只在同一塊緩衝區打轉,最後一次性貼回畫布。換句話說,就是把之前的坑補起來。

為什麼會一直在拷?

瀏覽器裡,JS 的 ArrayBuffer/Uint8Array 跟 Wasm 的線性記憶體是兩個世界。你呼叫一個 #[wasm_bindgen] fn foo(input: &[u8]) -> Vec<u8> 時,wasm-bindgen glue 會幫你把 JS 的 bytes 複製到 Wasm 記憶體,Rust 算完再把 Vec<u8> 複製回 JS。這對 API 設計很友善,但對大圖像來說,每次拷個幾 MB 就很慢很傷。

傳統改善方法是「把結果寫進呼叫者提供的 out buffer」(Day 13/14 你已經做過 *_into/in-place 版本),這能省去 Rust 內部的重複配置,但仍有兩個不可避免的跨界拷貝:JS→Wasm(把輸入塞進去)、Wasm→JS(把結果拿回來)。

真正的零拷:把像素常駐在 Wasm

Wasm 端配置一塊可復用的大緩衝區,整條管線都只在這塊上讀寫。JS 的角色變成:

  1. 先把一張圖的像素一次性「灌」進 Wasm 緩衝(只發生一次)。
  2. 調整參數、多次套用算子,都發生在 Wasm 內部,不再跨邊界。
  3. 要顯示時,直接從 Wasm 緩衝建一個 Uint8Array 視圖,把它貼回 ImageData(只發生一次)。

你在上一章已經把這套 API 實作出來了:

  • Rust(Wasm 端):ensure_buffer(cap), load_pixels(src), run_pipeline_inplace(w, h, ops), buffer_ptr(), buffer_len()
  • JS:用 WebAssembly.Memory 建立對 Wasm 緩衝的零拷貝視圖

它們合起來,就是一條0 次中途搬運的資料管線。

把流程接起來

1) 把像素灌進 Wasm 緩衝

// 來源像素(來自 canvas)
const imgData = ctx.getImageData(0, 0, w, h)
const bytes   = new Uint8Array(imgData.data.buffer)

// 確保 Wasm 端有足夠容量,然後一次 copy 進去
ensure_buffer(bytes.length)
load_pixels(bytes)   // ← 這是整條管線唯一的 JS→Wasm 拷貝

這一步之後,像素已經常駐在 Wasm 的 BUF,後面所有算子都會在 BUFSCR(暫存)之間來回「乒乓」而不是跨界搬。

2) 算子都關在 Wasm

await run_pipeline_inplace(w, h, ops)
// 這行裡面:grayscale → bc → blur … 都在 Wasm 端 BUF/SCR 互打,零跨界

你在 Rust 端用了 thread-local BUF/SCR,每一步只是在兩塊切片之間拷貝與運算。沒有任何 Vec<u8> 回傳給 JS,自然也沒有回傳拷貝

3) 把結果貼回畫布

const ptr  = buffer_ptr()
const len  = buffer_len()
const view = new Uint8Array((memory as WebAssembly.Memory).buffer, ptr, len)

imgData.data.set(view)  // ← 唯一的 Wasm→JS 拷貝
ctx.putImageData(imgData, 0, 0)

這裡不是把 Wasm 的資料「抓出來」,而是從 JS 端建立對 Wasm 記憶體的「觀景窗」view 指向的仍是 Wasm 的那塊線性記憶體;set(view) 這一下才做了最後一次拷貝,把結果貼到 ImageData

這樣真的零拷了嗎?

嚴格說是「中途零拷」。起點(JS→Wasm 載入)與終點(Wasm→JS 貼回畫布)各有一次不可避免的拷貝——因為 CanvasRenderingContext2D.putImageData 需要一份在 JS 世界的 ImageData。但介於兩者之間的 N 個算子,完全沒有跨界搬運。如果你的 UI 會在滑桿上連續觸發 30 次「亮度/對比」重算,傳統做法會有 30 次 JS↔Wasm 來回;現在只剩第一次與最後一次。

小結

這一章其實是 Day 10–15 的總結:算子可以再微調、迭代可以改成乒乓,但只要中途還在 JS/Wasm 之間傳陣列,你就等於在跑步機上拉著一台推車。把像素鎖在 Wasm 端、只在起點與終點各拷貝一次,你會看到整條鏈(載入→拉桿→顯示)都變輕。這時候,再去追求 5–10% 的卷積優化,才有「站在正確基礎上」的意義。


算是把坑填起來了?寫到現在突然想到其實一天也不用寫那麼多。話說,我錄取量化讀書會了耶,入會考真的超難,總共 16 題,10 題數學+基礎觀念,再加上 6 題的 Leetcode(? 題,我只寫出三還四題,有一題 Runtime,一題 TLE。還是我太久沒碰競程題目了哎


上一篇
Day 15|為什麼 .wasm 會變胖?怎麼減?Cargo 與 wasm-opt 的分工
下一篇
Day 17|把「住在你家」的位元組管理好
系列文
把前端加速到天花板:Rust+WASM 即插即用外掛18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言