使用 WebAssembly 取代應用程式中的熱路徑's JavaScript

方法持續快速,

先前中 介紹 WebAssembly 的文章 將 C/C++ 的程式庫生態系統提供到網路上。符合以下條件的應用程式: 大量使用 C/C++ 程式庫是 squoosh, 這種網頁應用程式可讓你以各種轉碼器壓縮圖片, 從 C++ 編譯為 WebAssembly

WebAssembly 是低階虛擬機器,可執行 於 .wasm 個檔案中。這個位元組程式碼屬於強型別,且結構明確 但 LLM 的編譯和最佳化作業速度比 主機系統更快 JavaScript 可以。WebAssembly 提供的環境可以執行 打從一開始就考慮採用沙箱機制和嵌入功能

以我的經驗來說,網路上大多數的效能問題是強制使 但重複繪製過多畫面 但現在每隔一段時間,應用程式都需要 需要耗費大量時間的運算成本高昂WebAssembly 可以協助 此處。

熱門路徑

其實我們寫了一個 JavaScript 函式 可將圖像緩衝區旋轉 90 度的倍數。雖然 「OffscreenCanvas」是 原因是我們指定的所有瀏覽器都不支援這個功能 Chrome 發生錯誤

這個函式會疊代輸入圖片的每個像素,並將其複製到 輸出圖像中的不同位置來旋轉圖像。4094px x 4096 像素 (1600 萬像素) 的圖片,需要超過 1,600 萬個 內部程式碼區塊,也就是我們所謂的「熱路徑」就算尺寸較大 我們分別在三個瀏覽器中完成測試 秒或更短的時間。可接受的互動時間長度。

for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    const in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

不過,其中一個瀏覽器可耗時超過 8 秒。瀏覽器針對 JavaScript 進行最佳化的方式 因為其實相當複雜,不同的引擎會針對不同的用途進行最佳化。 有些則針對原始執行進行最佳化,有些則針對與 DOM 的互動進行最佳化。於 也就是某個瀏覽器中未最佳化的路徑

另一方面,WebAssembly 的建構完全是以原始的執行速度為基礎,以下內容 要實現跨瀏覽器快速、「可預測」的效能,以便執行像這樣的程式碼, WebAssembly 可以幫助您。

利用 WebAssembly 實現可預測的效能

一般來說,JavaScript 和 WebAssembly 能夠達到相同的最佳效能。 不過,對於 JavaScript 來說,這個效能只能透過「快速路徑」取得。 維持「快速路徑」通常並不容易這項主要優點是 WebAssembly 提供可預測的效能,就算在不同瀏覽器上也能有效預測。嚴格 打字和低階架構都能讓編譯器變得更加強大 可保證 WebAssembly 程式碼只需最佳化一次 請一律使用「快速路徑」。

為 WebAssembly 編寫程式碼

我們先前利用 C/C++ 程式庫並將其編譯為 WebAssembly, 網路功能我們真的沒碰到圖書館的程式碼 只編寫少量的 C/C++ 程式碼,形成瀏覽器之間的橋樑 和程式庫這次我們的動機不同:我們想寫字 使用 WebAssembly 從零開始 WebAssembly 的優勢

WebAssembly 架構

撰寫「針對 WebAssembly」時,如果能深入瞭解一些 WebAssembly 的本質上

如何引用 WebAssembly.org

將一段 C 或 Rust 程式碼編譯至 WebAssembly 時,可取得 .wasm 內含模組宣告的檔案這項宣告包含 「匯入」模組應來自其環境,則此清單會列出 模組可提供給主機 (函式、常數、記憶體區塊) 使用 當然,其中所含函式的實際二進位指示。

經過仔細研究後我才發現到:層出不窮的堆疊 WebAssembly 是「堆疊式虛擬機器」未儲存在 以及 WebAssembly 模組使用的記憶體堆疊完全屬於 VM 內部 網頁開發人員無法存取 (開發人員工具除外)。因此 編寫完全不需額外記憶體的 WebAssembly 模組 僅使用 VM 內部堆疊

在此情況下,我們需要使用一些額外的記憶體,以允許任意存取 並產生該圖像的旋轉版本。這是 「WebAssembly.Memory」的用途

記憶體管理

通常在使用額外記憶體後,您會需要 來管理這些記憶體正在使用記憶體的哪些部分?哪些是免費的? 例如,在 C 中,malloc(n) 函式會尋找記憶體空間 共計 n 個位元組。此類型的函式也稱為「配置器」。 當然,採用的配置器也必須包含在 WebAssembly 模組,且會增加您的檔案大小。這個大小和效能 當中的記憶體管理功能 所以許多語言提供多個實作 可選擇 (「dmalloc」、「emmalloc」、「wee_alloc」等)。

在這個範例中,我們知道輸入圖片的尺寸 ( 輸出影像的尺寸),再執行 WebAssembly 模組。我們在這裡 我們看到了商機:一般來說,我們會將輸入圖像的 RGBA 緩衝區做為 參數傳送至 WebAssembly 函式,並將旋轉的圖片傳回,做為傳回 值。如要產生傳回值,我們必須使用配置器。 不過,因為我們知道所需的記憶體總量 (輸入大小是輸入量的兩倍 分別用於輸入和輸出),我們可以將輸入圖片放入 使用 JavaScript 的 WebAssembly 記憶體,執行 WebAssembly 模組來產生 第二,旋轉圖片,然後使用 JavaScript 來讀回結果。我們可以 不必管理任何記憶體!

已停用

如果您查看原始 JavaScript 函式 會讓 WebAssembly-fy 這個模型 不含 JavaScript 專屬 API 的程式碼因此這應該相當直觀 再將這個程式碼轉攜至任何語言我們評估了 3 種不同的語言 所編譯的 WebAssembly:C/C++、Rust 和 AssemblyScript。唯一的問題 我們需要針對每一種語言回答:如何存取原始記憶體 而不使用記憶體管理功能?

C 和 Emscripten

Emscripten 是 WebAssembly 目標的 C 編譯器,Emscripten 的目標是 函式做為 GCC 或 clang 等知名 C 編譯器的置入式取代 且大部分與標記相容這是 Emscripten 的使命核心 因為我們希望您能像一樣輕鬆將現有 C 和 C++ 程式碼編譯到 WebAssembly

存取原始記憶體是 C 的本質,而指標也是為了要達成這個目的 原因:

uint8_t* ptr = (uint8_t*)0x124;
ptr[0] = 0xFF;

這裡,我們要將數字 0x124 轉換成 8 位元未簽署的指標 整數值 (或位元組)。這實際上會將 ptr 變數轉換成陣列 從記憶體位址 0x124 開始,我們可以使用此位址和其他任何陣列 允許我們存取個別位元組進行讀取和寫入在這個範例中 也就是我們希望透過 RGBA 緩衝區重新訂購的圖像 並輪替金鑰如要移動像素,我們實際上必須連續移動 4 個位元組 (每個管道一個位元組:R、G、B 和 A)。為了方便起見 32 位元整數陣列。按照慣例,輸入圖片 地址 4,我們的輸出圖片將在輸入圖片之後直接開始 結束時間:

int bpp = 4;
int imageSize = inputWidth * inputHeight * bpp;
uint32_t* inBuffer = (uint32_t*) 4;
uint32_t* outBuffer = (uint32_t*) (inBuffer + imageSize);

for (int d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (int d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    int in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}
敬上

將整個 JavaScript 函式移植到 C 後,我們就可以編譯 C 檔案 主講者:emcc

$ emcc -O3 -s ALLOW_MEMORY_GROWTH=1 -o c.js rotate.c

一如以往,emscripten 會產生名為 c.js 的黏附程式碼檔案和 wasm 模組 名為 c.wasm。請注意, wasm 模組 gzip 只能存放約 260 位元組,而 gzip 後的 glue 程式碼約為 3.5 KB。搞怪後,我們成功收購 黏合程式碼,並使用基本 API 將 WebAssembly 模組例項化。 使用 Emscripten 通常只要不利用任何東西即可 來自 C 標準程式庫

Rust

Rust 是全新的現代程式設計語言,具備豐富的型別系統,無執行階段 以及確保記憶體安全和執行緒安全的擁有權模型鐵鏽色 另外,WebAssembly 做為核心功能 Rust 團隊 為 WebAssembly 生態系統提供了許多卓越的工具

這些工具就是 wasm-packrustwasm 工作團隊wasm-pack 可以將程式碼轉換為適合網頁使用的模組 立即可用的套裝組合,包括 webpack 等整合工具。「wasm-pack」是極佳 使用起來相當便利,但目前僅適用於 Rust。群組: 考慮新增其他 WebAssembly 指定語言支援。

在 Rust 中,切片是指 C 中包含的陣列。就像在 C 中一樣 使用起始地址的切片在記憶體安全模式下 所以 Rust 會強制執行,所以為了瞭解需要使用 unsafe 關鍵字, 讓我們能夠撰寫不符合模型的程式碼。

let imageSize = (inputWidth * inputHeight) as usize;
let inBuffer: &mut [u32];
let outBuffer: &mut [u32];
unsafe {
    inBuffer = slice::from_raw_parts_mut::<u32>(4 as *mut u32, imageSize);
    outBuffer = slice::from_raw_parts_mut::<u32>((imageSize * 4 + 4) as *mut u32, imageSize);
}

for d2 in 0..d2Limit {
    for d1 in 0..d1Limit {
    let in_idx = (d1Start + d1 * d1Advance) * d1Multiplier + (d2Start + d2 * d2Advance) * d2Multiplier;
    outBuffer[i as usize] = inBuffer[in_idx as usize];
    i += 1;
    }
}

使用以下程式碼編譯 Rust 檔案

$ wasm-pack build

會產生約 100 位元組的黏合程式碼 (兩者都是 gzip 之後) 的 7.6 KB 模組。

AssemblyScript

AssemblyScript 是相當公平的做法 漸進式專案,目標是成為 TypeScript-to-WebAssembly 編譯器。是 但必須注意的是,不光是使用任何 TypeScript。 AssemblyScript 使用的語法與 TypeScript 相同,但會停用標準程式碼 資源庫他們的標準程式庫模擬了 WebAssembly。換句話說,您不能只編譯自己指向的任何 TypeScript WebAssembly 的外掛程式 但「確實」 撰寫 WebAssembly 程式語言!

    for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
      for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
        let in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
        store<u32>(offset + i * 4 + 4, load<u32>(in_idx * 4 + 4));
        i += 1;
      }
    }

考量到 rotate() 函式擁有的小型型別介面,它是 就能輕鬆將這個程式碼移植到 AssemblyScriptload<T>(ptr: usize)store<T>(ptr: usize, value: T) 函式是由 AssemblyScript 提供, 存取原始記憶體如要編譯 我們的 AssemblyScript 檔案, 我們只需要安裝 AssemblyScript/assemblyscript npm 套件並執行

$ asc rotate.ts -b assemblyscript.wasm --validate -O3

AssemblyScript 會提供約 300 位元組的 wasm 模組,以及沒有黏附程式碼。 這個模組只能與基本 WebAssembly API 搭配使用。

WebAssembly 鑑識

與另外 2 種程式語言相比,Rust 的 7.6 KB 驚訝地超出了預期。有 WebAssembly 生態系統中有幾項工具 您的 WebAssembly 檔案 (無論其建立的語言為何),和 讓您瞭解情況,並協助您改善目前的情況。

托維吉

Twiggy 是 Rust 的另一項工具 WebAssembly 團隊會從 WebAssembly 擷取大量精闢的深入分析結果 後續課程我們將逐一介紹 預先訓練的 API、AutoML 和自訂訓練這項工具並非 Rust 專用,且可讓您檢查 模組的呼叫圖,判斷未使用或超流的區段,然後找出 構成模組總檔案大小的區段 後者可透過 Twiggy 的 top 指令完成:

$ twiggy top rotate_bg.wasm
Twiggy 安裝螢幕截圖

在這個範例中,我們發現大多數的檔案大小來自 配置器。這個情況讓人感到驚訝,因為我們的程式碼並未使用動態分配。 另一個影響因素是「函式名稱」子節。

Wam-Strip

wasm-strip」是 WebAssembly Binary Toolkit 提供的工具,簡稱為 Wabt。這個 SDK 包含 數種能檢查及操控 WebAssembly 模組的工具 wasm2wat 是一種反組譯工具,可將二進位 wasm 模組轉換成 人類可讀的格式Wabt 也包含 wat2wasm,可讓你轉動 人類可讀的格式傳回二進位 wasm 模組雖然我們確實是在 這兩個相輔相成的工具可以檢查 WebAssembly 檔案 wasm-strip 最實用。wasm-strip 會移除不必要的區段 從 WebAssembly 模組擷取與中繼資料

$ wasm-strip rotate_bg.wasm

這會將信任模組的檔案大小從 7.5 KB 縮減為 6.6 KB (gzip 之後)。

wasm-opt

wasm-optBinaryen 的工具。 這項服務需要 WebAssembly 模組,並嘗試針對 僅以位元碼為依據。某些工具,例如 Emscripten 有些則沒有。通常建議您可以嘗試儲存一些 額外增加位元組

wasm-opt -O3 -o rotate_bg_opt.wasm rotate_bg.wasm

有了 wasm-opt,我們就能省下更多位元組 gzip 後的 6.2 KB。

#![no_std]

經過諮詢和研究後,我們重新編寫 Rust 程式碼,不使用 Rust 的標準程式庫 (採用 #![no_std]敬上 而不是每個特徵的分數這樣會一併停用動態記憶體配置功能,移除 擷取自解碼器程式碼編譯這個 Rust 檔案 同時

$ rustc --target=wasm32-unknown-unknown -C opt-level=3 -o rust.wasm rotate.rs

wasm-optwasm-strip 和 gzip 之後產生一個 1.6 KB 的 wasm 模組。這件事 遠大於 C 和 AssemblyScript 產生的模組 足以視為輕量級

成效

在我們單憑檔案大小展開結論之前,我們先踏上這段旅程 以便取得最佳效能,而非檔案大小。我們如何評估成效 結果如何?

如何基準化

WebAssembly 是低階位元碼格式,仍必須傳送 來產生主機專屬機器碼就跟 JavaScript 一樣 編譯器會在多個階段運作簡單來說,第一個階段是 但程式碼編譯速度通常較慢模組開始後 瀏覽器會觀察哪些部分經常使用,然後將那些部分 帶來最佳化但速度較慢的編譯器

我們的使用案例很有趣的:系統會使用旋轉圖片的程式碼 也許兩次在大多數的情況下,我們從未取得 優點請特別注意 基準化。如果在迴圈中執行 WebAssembly 模組 10,000 次, 不切實際的結果為取得實際數字,我們應執行模組一次 做出決策

成效比較

各語言的速度比較
各瀏覽器的速度比較

這兩張圖表是不同資料的檢視方式。在第一個圖表中 在第二張圖表中比較各個瀏覽器使用的語言。請 請注意,我選擇了對數時間軸另外,請務必確保 基準測試使用相同的 1600 萬像素測試圖片和相同的主機 除了單一瀏覽器以外,因為單一瀏覽器無法在同一部電腦上執行。

如果不深入分析這些圖表 我們顯然解除了原始的 效能問題:所有 WebAssembly 模組的執行時間約在 500 毫秒以內。這個 確立了我們一開始所言的:WebAssembly 可以提供可預測 才需進行無論我們選擇哪種語言,各瀏覽器之間的差異。 而且就是最基本準確:JavaScript 的標準差 所有瀏覽器的大小約為 400 毫秒,但所有瀏覽器的標準差 所有瀏覽器的 WebAssembly 模組約為 80 毫秒,

難度

另一項指標是我們耗費許多心力 WebAssembly 模組進入 squoosh 之後難以將數值指派給 所以我不會製作任何圖表,但有些項目需要調整 請說明:

AssemblyScript 的過程順暢無阻。還能使用 TypeScript 編寫 WebAssembly,方便我的同事輕鬆審查程式碼 產生無膠水的 WebAssembly 模組 才需進行TypeScript 生態系統中的工具,例如更美觀與扭曲的 應該就能正常運作

將 Rust 與 wasm-pack 搭配使用也非常方便,但特別適合 WebAssembly 專案擁有更多繫結和記憶體管理 。我們必須讓大家知道「開心」這個方式,才能取得競爭優勢 檔案大小

C 和 Emscripten 建立了非常小且高效能的 WebAssembly 模組 立即可用,但沒有勇氣跳進膠水程式碼,進而減少使用 但不需要總大小 (WebAssembly 模組 + 黏附程式碼) 就能達到 也變得相當大

結論

因此,如果您有 JS 熱路徑,並想要讓路徑 或是更一致的 WebAssembly如同成效一般 答案是:一切都取決於那麼我們製造什麼東西呢?

比較圖表

比較不同語言的模組大小 / 效能取捨 最理想的選擇似乎是 C 或 AssemblyScript。我們決定運送 Rust。有 造成這項選擇的原因有很多:到目前為止已經出貨了所有 Squoosh 的轉碼器 是使用 Emscripten 進行編譯我們想拓展我們對 WebAssembly 生態系統和在實際工作環境中使用不同語言。 AssemblyScript 是一種強而有力的替代方案,但專案相對較新, 因此編譯器不像 Rust 編譯器那麼成熟。

Rust 和其他語言大小的檔案大小差異 在散佈圖中看起來好像很滑,但事實並非如此: 即使透過 2G 載入 500B 或 1.6 KB,也只需不到 1/10 秒就能載入完成。且 Rust 希望很快就能消弭模組大小的差距。

就執行階段效能而言,Rust 在所有瀏覽器上的平均速度比 AssemblyScript。尤其是在規模較大的專案上 產生更便捷的程式碼,不需手動最佳化程式碼。但那 就會跳脫自己最適用的選擇

說到這裡,AssemblyScript 是一項絕佳的發現。允許 開發人員不必學習 語言。AssemblyScript 團隊的回應速度非常快,而且積極參與 致力改善工具鍊我們一定會隨時留意 AssemblyScript。

更新:Rust

本文發布後,Nick Fitzgerald Rust 團隊提供的實用 Rust Wasm 精心撰寫,其中含有 最佳化檔案大小一節。請確實按照 操作說明 (最值得注意的是,連結時間最佳化和手動 恐慌處理) 讓我們可以編寫「一般」Rust 程式碼,並改回使用 Cargo (Rust 的 npm),且不會縮減檔案大小。Rust 模組結束時 安裝 gzip 後產生 3,70B 美金詳情請參閱 Squoosh 開啟的 PR

特別感謝 Ashley WilliamsSteve KlabnikNick FitzgeraldMax Graey 帶到這段旅程的夥伴。