做這個套件,算子本身通常是 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 端配置一塊可復用的大緩衝區,整條管線都只在這塊上讀寫。JS 的角色變成:
Uint8Array
視圖,把它貼回 ImageData
(只發生一次)。你在上一章已經把這套 API 實作出來了:
ensure_buffer(cap)
, load_pixels(src)
, run_pipeline_inplace(w, h, ops)
, buffer_ptr()
, buffer_len()
WebAssembly.Memory
建立對 Wasm 緩衝的零拷貝視圖它們合起來,就是一條0 次中途搬運的資料管線。
// 來源像素(來自 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
,後面所有算子都會在 BUF
與 SCR
(暫存)之間來回「乒乓」而不是跨界搬。
await run_pipeline_inplace(w, h, ops)
// 這行裡面:grayscale → bc → blur … 都在 Wasm 端 BUF/SCR 互打,零跨界
你在 Rust 端用了 thread-local BUF
/SCR
,每一步只是在兩塊切片之間拷貝與運算。沒有任何 Vec<u8>
回傳給 JS,自然也沒有回傳拷貝。
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。還是我太久沒碰競程題目了哎