天天看點

為 Winch 提供對 Wasm 指令的支援(譯文-來自:Shopify)

作者:閃念基因

一個月前,我向 Wasmtime 的基線(非優化)編譯器 Winch 發出了拉取請求,以添加對許多 WebAssembly 指令的支援。Shopify 正在開發 Winch,以使 Wasm 編譯速度更快。我學習了代碼庫的許多重要部分以及如何測試我所做的更改,我将在下面概述。

添加對 WebAssembly 指令的支援意味着,當 Winch 遇到給定指令時,它會發出零個或多個本機機器代碼指令來執行操作,同時確定 WebAssembly 堆棧處于正确的狀态并且 CPU 寄存器得到适當的配置設定或釋放。

這是一個有些複雜的過程,是以這裡是所涉及更改的快速摘要:

  1. 将指令添加到visitor并調用 并MacroAssembler可能CodeGenContext
  2. 向彙編器添加一個方法,MacroAssembler如果彙編器上尚未定義适當的方法,則該方法會調用彙編器上的一個或多個方法MacroAssembler
  3. 可能向其中添加一個輔助方法,CodeGenContext以確定在發出機器指令以對操作數進行操作之前,已将指令的操作數從堆棧彈出并放入寄存器中
  4. 将方法添加到發出 x86_64 機器指令的彙編器
  5. 為新指令添加一些測試并檢查生成的代碼
  6. 将新指令添加到支援模糊測試的指令清單中
  7. 執行模糊測試至少幾分鐘(并可能調查和修複故障)

這是一個進階圖表,顯示了我将讨論的各個部分之間的關​系:

為 Winch 提供對 Wasm 指令的支援(譯文-來自:Shopify)

為了添加對指令的支援,我首先更改了visitorWinch 的codegen子產品。這visitor是通路者設計模式的實作,用于周遊 Wasm 函數的解析指令。該wasmparser闆條箱可以解析 WebAssembly 子產品。

WebAssembly 子產品由有序的部分清單組成,例如類型、代碼或導入部分。為了添加對指令的支援,我們重點關注代碼部分的解析方式。代碼部分包含代碼條目的集合。部分代碼條目是函數。函數的一部分是表達式代表函數體。表達式是一系列零個或多個指令,後跟訓示表達式已結束的操作碼。解析表達式時,wasmparser可以配置為調用其VisitOperator特征的實作。這visitor是 Winch 對VisitOperatorTrait 的實作。該VisitOperator特征具有與每個 WebAssembly 指令相對應的通路方法。添加對指令的支援涉及在VisitOperator實作中添加适當通路方法的實作。visitor和CodeGenContext的代表們MacroAssembler。

對通路者的更改如下所示:

fn visit_i32_clz(&mut self) {
use OperandSize::*;
self.context.unop(self.masm, S32, &mut |masm, reg, size| {
masm.clz(reg, reg, size);
});
}           

此更改增加了對i32.clzWasm 指令的支援,該指令計算堆棧頂部值中前導零的數量。

坐标CodeGenContext寄存器配置設定器、值堆棧和目前函數的架構。值堆棧上的值可以是常量、寄存器、WebAssembly 本地索引或線性記憶體中的偏移量。寄存器配置設定器負責跟蹤目前配置設定了哪些寄存器,并可以配置設定或釋放寄存器。如果寄存器不可用并且被請求,它也可能溢出值。洩漏涉及:

  1. 将值從寄存器移動到線性記憶體中的偏移量
  2. 更新值堆棧上對該寄存器的引用,以指向記憶體中現在存儲該值的偏移量

這會釋放寄存器,同時保留堆棧上的值。然而,由于溢出會導緻額外的操作,是以我們希望在執行操作時避免使用不必要的寄存器,以減少需要溢出的機會。hasCodeGenContext方法確定正确數量的操作數已從堆棧中彈出,并作為立即數傳遞(如果可能)或加載到寄存器中。立即數是機器指令中使用的常量。例如,在指令 中add eax, 10,10是立即數。中的代碼CodeGenContext獨立于指令集架構(ISA),而不獨立于 ISA 的代碼則放在MacroAssembler.

它MacroAssembler是一個獨立于 ISA 的接口,用于發出與 WebAssembly 指令相對應的機器代碼指令。x86_64(Intel 和 AMD CPU)和AArch64(Apple Silicon)是 ISA 的示例。添加對 Wasm 指令的支援i32.and涉及and向MacroAssembler. MacroAssemblerWinch 支援的每個 ISA都有一個實作。這允許不同的 ISA 為相同的 WebAssembly 指令發出不同的機器指令。委托MacroAssembler給特定于 ISA 的彙編器來發出實際的機器指令。

在簡單情況下,WebAssembly 指令可以由單個機器代碼指令實作,并且該機器代碼指令的操作數不需要放置在特定寄存器中,可以簡單地調用彙編器上的等效方法MacroAssembler。在更複雜的情況下,MacroAssembler可能需要将操作數放置在特定的寄存器中。例如,在實作左移(即shl)時,如果要移動的位數的操作數不是作為立即數傳遞的,則需要将其放入寄存器中cl。這需要在MacroAssembler. 此外,MacroAssembler可能需要對彙編器進行一系列調用以發出許多指令。例如,在實作 Wasm 的eq指令時,x86_64 cmp必須發出指令,然後必須發出指令以将狀态寄存器中的零标志的值複制到目标寄存器。

在更複雜的情況下,MacroAssembler可能需要檢查為其編譯機器代碼的 CPU 是否支援給定的指令,如果缺少支援,則替換不同的指令集。例如,Wasm 的clz(計數前導零)指令可以通過使用x86_64的lzcnt(計數前導零)指令來實作,但某些x86_64 CPU 不支援lzcnt,是以我們檢查該支援是否可用,并訓示MacroAssembler彙編器發出如果不提供支援,則輸出相同結果的一系列指令。

MacroAssembler 的更改示例如下:

fn clz(&mut self, src: Reg, dst: Reg, size: OperandSize) {
    if self.flags.has_lzcnt() {
        self.asm.lzcnt(src, dst, size);
    } else {
        let scratch = regs::scratch();
        
        // Use the following approach:
        // dst = size.num_bits() - bsr(src) - is_not_zero
        //     = size.num_bits() + -bsr(src) - is_not_zero.
        self.asm.bsr(src.into(), dst.into(), size);
        self.asm.setcc(CmpKind::Ne, scratch.into());
        self.asm.neg(dst, dst, size);
        self.asm.add_ir(size.num_bits(), dst, size);
        self.asm.sub_rr(scratch, dst, size);
    }
}           

該clz指令計算操作數的二進制表示形式中前導零的數量。如果我們正在編譯的 CPU 上可用,我們将使用x86_64的(前導零計數)指令。lzcnt否則,我們回退到使用(位掃描反向)的實作bsr,該實作找到最高有效位集的索引。如果該bsr指令的操作數為 0,則在狀态寄存器中設定零标志位,否則清除零标志位。setne如果零标志位為 0,則該指令将 0 複制到暫存寄存器,否則複制 1。以 結尾的彙編器方法_ir表示它們作用于立即數和寄存器,以 結尾的彙編器方法表示它們作用于立即數和寄存器。_rr表明它們作用于兩個寄存器。我還必須調整執行減法的方式,因為無法從立即值中減去寄存器。

彙編器公開與底層機器指令集一緻的方法。它建立在 Cranelift 的機器代碼發射之上。Cranelift 是 Wasmtime 使用的低級代碼生成器(即對機器代碼進行最少抽象的代碼)。還可能有專門用于擷取一對寄存器或一個立即數和一個寄存器的方法。

為了确定可用的機器指令,我結合使用了 ChatGPT、Google 和 Cranelift 的 Codegen 的x86_64ISLE 定義。Cranelift 的 ISLE 是一種基于 S 表達式的領域特定語言,用于将指令降低為機器代碼。這些定義通過代碼生成運作,以輸出彙編器使用的 Rust 代碼。這些定義具有有用的源代碼級注釋,為所定義的指令的作用提供了額外的上下文。我首先嘗試通過 ISLE 定義使用 cmd+f 來檢視可以找到與給定操作相關的内容(例如,or)。

如果我找不到明顯的東西,我會詢問 ChatGPT 它如何以x86_64彙編語言實作給定 WebAssembly 指令的描述。然後我會在 Google 上查找 ChatGPT 建議的說明,以驗證建議的準确性。在某些情況下,x86_64ChatGPT 建議的彙編代碼略有錯誤。然後我嘗試在 ISLE 定義中找到與建議的x86_64彙編指令相比對的條目。然後,我将編寫一個或多個彙編器方法來調用為該 ISLE 定義生成的代碼,以發出帶有正确參數的機器指令。

ISLE 定義的示例bsr如下lzcnt:

(type UnaryRmROpcode extern
(enum Bsr
Bsf
Lzcnt
Tzcnt
Popcnt))

;; Instructions on general-purpose registers that only read src and
;; defines dst (dst is not modified). `bsr`, etc.
(UnaryRmR (size OperandSize) ;; 2, 4, or 8
(op UnaryRmROpcode)
(src GprMem)
(dst WritableGpr))           

該UnaryRmR指令是我将參考的内容,并且我将通過Bsr或Lzcnt作為op執行的内容。

彙程式設計式中的更改示例如下:

/// Store the count of leading zeroes in src in dst.
/// Requires `has_lzcnt` flag.
pub fn lzcnt(&mut self, src: Reg, dst: Reg, size: OperandSize) {
    assert!(self.isa_flags.has_lzcnt(), "Requires has_lzcnt flag");
    self.emit(Inst::UnaryRmR {
        size: size.into(),
        op: args::UnaryRmROpcode::Lzcnt,
        src: src.into(),
        dst: dst.into(),
    });
}           

此更改添加了發出lzcnt指令的方法。Inst::UnaryRmR引用上面 Cranelift 的 ISLE 定義生成的代碼。

測試新的更改也與典型的軟體開發有很大不同。自動化測試有兩種形式:檔案測試和模糊測試。檔案測試使用特殊格式的 WebAssembly 文本格式檔案(檔案.wat),其中目标架構和 CPU 标志被指定為注釋,然後是 WebAssembly 子產品,最後是我們期望生成的機器代碼。因為這些測試隻是生成和檢查代碼而不執行它,是以它們可以在AArch64機器上運作,比如我們的開發 Macbook。

通常,我編寫前兩部分,然後運作測試并檢查機器代碼輸出是否符合我的預期。特别是對于x86_64,我檢查寄存器是否具有正确的寬度(以e對于 32 位和以 r 開頭的寄存器(對于 64 位),将發出預期的x86_64指令,并在可能的情況下使用立即數。我編寫了一組使用常量、函數參數、局部變量、期望發出立即數和期望寄存器的變體的測試,以及測試的 32 位和 64 位版本。當我重構以檢視它是否更改了發出的機器代碼時,這些測試也很有幫助。這些檔案測試無法斷言發出的機器指令是否會産生正确的輸出。為此,我們使用差分模糊測試。

檔案測試的示例如下所示:

;;; Additional comments starting with `;;;` have been added to provide additional context.
;;; These comments would need to be removed to run the filetest.
;;; This filetest tests what instructions get emitted when running `i32.clz` on a constant of `1`.
;;! target = "x86_64"
;;! flags = ["has_lzcnt"]

(module
(func (result i32)
(i32.const 1)
(i32.clz)
)
)
;;; The first four lines set up the frame pointer and stack pointer registers.
;; 0: 55 push rbp
;; 1: 4889e5 mov rbp, rsp
;; 4: 4883ec08 sub rsp, 8
;; 8: 4c893424 mov qword ptr [rsp], r14
;;; Move the constant of `1` into the `eax` register.
;; c: b801000000 mov eax, 1
;;; Run lzcnt.
;; 11: f30fbdc0 lzcnt eax, eax
;;; Revert the changes to the frrame pointer and stack pointer registers as part of returning from the function.
;; 15: 4883c408 add rsp, 8
;; 19: 5d pop rbp
;; 1a: c3 ret           

差分模糊背後的想法是它随機生成無限系列的 WebAssembly 子產品,并且它将使用随機選擇的輸入調用其生成的每個子產品中的函數。它将使用 Winch 并使用不同的 WebAssembly 引擎在特定 Wasm 子產品上運作特定輸入。然後它會比較結果,如果不同則崩潰。與使用固定輸入的內建測試相比,這可以測試更廣泛的輸入。它确實幫助我發現了一些我本來會錯過的問題。缺點是用于測試x86_64指令的差分模糊測試必須在具有x86_64處理器的機器上執行。要模糊 Winch 指令,需要将該指令添加到winch_supports_module差分模糊目标中的比對語句。原因是 Winch 不支援所有 Wasm 指令,如果生成的子產品使用尚不支援的指令,則生成的子產品将會失敗,是以我們不會模糊包含不支援的指令的子產品。此比對聲明用于表明我們确實支援列出的說明。

在確定檔案測試的輸出看起來合理并且差異模糊測試運作至少幾分鐘後,我在我的 Wasmtime 個人分支上打開了草稿 PR,并将其合并到我的個人分支(不是上遊)中,這樣我就可以收集有關的内部回報我的改變。在疊代該回報後,我重新調整為一次送出,并在公共 Wasmtime 存儲庫上打開了一個 PR。

作者:

Jeff Charles 是 Shopify Wasm 基礎團隊的進階開發人員。您可以在 GitHub 上找到他(@jeffcharles),或者在 LinkedIn 上找到他(Jeff Charles)。

出處:https://shopify.engineering/contributing-support-for-a-wasm-instruction-to-winch

繼續閱讀