LLVM 中對 AArch64 可擴展矩陣擴展的支援

1. 簡介

AArch64 SME ACLE 為使用者提供了一些屬性來控制 PSTATE.SM 和 PSTATE.ZA。 AArch64 SME ABI 描述了當至少一個函數使用 PSTATE.SM 或 PSTATE.ZA 時,函數之間呼叫的要求。

本文檔描述了 SME ACLE 屬性如何映射到 LLVM IR 屬性,以及 LLVM 如何降低這些屬性以實現 ABI 的規則和要求。

以下我們描述 LLVM IR 屬性及其與 C/C++ 級別 ACLE 屬性的關係

aarch64_pstate_sm_enabled

用於具有 __arm_streaming 的函數

aarch64_pstate_sm_compatible

用於具有 __arm_streaming_compatible 的函數

aarch64_pstate_sm_body

用於具有 __arm_locally_streaming 的函數,並且僅在函數定義(而非聲明)上有效

aarch64_new_za

用於具有 __arm_new("za") 的函數

aarch64_in_za

用於具有 __arm_in("za") 的函數

aarch64_out_za

用於具有 __arm_out("za") 的函數

aarch64_inout_za

用於具有 __arm_inout("za") 的函數

aarch64_preserves_za

用於具有 __arm_preserves("za") 的函數

aarch64_expanded_pstate_za

用於具有 __arm_new_za 的函數

Clang 必須確保將上述屬性添加到函數的聲明/定義及其呼叫站點。這對於呼叫屬性函數指標非常重要,因為沒有可用的定義或聲明。

2. 處理 PSTATE.SM

當更改 PSTATE.SM 時,FP/向量運算的執行可能會轉移到另一個處理元件。這有三個重要的含義

  • 運行時 SVE 向量長度可能會更改。

  • FP/AdvSIMD/SVE 寄存器的內容將被清零。

  • 允許的指令集會發生變化。

這導致對 IR 和優化有一些限制。例如,在可能使用不同 PSTATE.SM 值運作的函數之間共用向量長度相依狀態是未定義行為。前端在產生 LLVM IR 時必須遵守這些限制。

即使執行階段 SVE 向量長度可能會改變,但就 LLVM IR 和 CodeGen 的幾乎所有部分而言,我們可以假設 vscale 的執行階段值不會改變。如果我們讓編譯器在呼叫邊界周圍插入適當的 smstartsmstop 指令,則可以減輕對 SVE 狀態的影響。透過將狀態改變限制在呼叫周圍非常短的時間範圍內,我們可以控制操作的排程方式,以及如何在狀態轉換之間保留即時值。

為了在這個粒度級別控制 PSTATE.SM,我們使用函數和呼叫站點屬性,而不是內建函數。

屬性限制

  • 將(指向)可縮放向量物件傳遞或返回到/自可能使用不同 SVE 向量長度的函數是未定義行為。這包括具有非串流介面,但標記為 aarch64_pstate_sm_body 的函數。

  • 不允許使用 aarch64_pstate_sm_compatibleaarch64_pstate_sm_enabled 這兩個屬性來裝飾函數。

  • 不允許使用下列多個屬性來裝飾函數:aarch64_new_zaaarch64_in_zaaarch64_out_zaaarch64_inout_zaaarch64_preserves_za

這些限制也適用於較高階的 SME ACLE,這表示我們可以在 Clang 中發出診斷訊息,以向使用者發出有關錯誤行為的信號。

編譯器插入的串流模式改變

下表描述了編譯器在具有不同屬性的函數之間進行呼叫時必須考慮的 PSTATE.SM 轉換。在此表中,我們使用以下縮寫

N

具有標準介面的函數(進入時 PSTATE.SM=0,返回時 PSTATE.SM=0)

S

具有串流介面的函數(進入時 PSTATE.SM=1,返回時 PSTATE.SM=1)

SC

具有串流相容介面的函數(進入時 PSTATE.SM 可以是 0 或 1,返回時保持不變)。

具有 __attribute__((arm_locally_streaming)) 的函數不包括在此表中,因為對於呼叫者來說,該屬性與「串流」同義,而對於被呼叫者來說,它只是一個未明確公開給呼叫者的實現細節。

表 4 具有不同屬性的函數的呼叫組合

來源

目標

呼叫前

呼叫後

例外發生後

N

N

N

S

SMSTART

SMSTOP

N

SC

S

N

SMSTOP

SMSTART

SMSTART

S

S

SMSTART

S

SC

SMSTART

SC

N

如果呼叫前 PSTATE.SM 為 1,則 SMSTOP

如果呼叫前 PSTATE.SM 為 1,則 SMSTART

如果呼叫前 PSTATE.SM 為 1,則 SMSTART

SC

S

如果呼叫前 PSTATE.SM 為 0,則 SMSTART

如果呼叫前 PSTATE.SM 為 0,則 SMSTOP

如果呼叫前 PSTATE.SM 為 1,則 SMSTART

SC

SC

如果呼叫前 PSTATE.SM 為 1,則 SMSTART

因為更改 PSTATE.SM 會將 FP/向量寄存器歸零,所以最好在寄存器分配之前發出 smstartsmstop 指令,以便寄存器分配器可以在模式更改前後儲存/載入寄存器。

編譯器也應該充分瞭解哪些操作是呼叫/函式參數/結果的一部分,以及哪些操作是函式主體的一部分,以便它能夠將模式更改精確地放置在正確的位置。 執行此操作的適當位置似乎是 SelectionDAG,它會在此處降低呼叫的參數/返回值以實現指定的呼叫約定。 SelectionDAG 提供了鏈和膠水來指定操作順序,並對指令的排程進行初步控制。

保存狀態的範例

當傳遞 float 值到具有串流介面的函式或從中返回 float 值到具有正常介面的函式時,呼叫端需要確保參數/結果暫存器被保存,並且在 smstart/smstop 和呼叫之間沒有其他程式碼被排程。

define float @foo(float %f) nounwind {
  %res = call float @bar(float %f) "aarch64_pstate_sm_enabled"
  ret float %res
}

declare float @bar(float) "aarch64_pstate_sm_enabled"

程式需要在暫存器 s0 中保存浮點參數和返回值。

foo:                                    // @foo
// %bb.0:
        stp     d15, d14, [sp, #-80]!           // 16-byte Folded Spill
        stp     d13, d12, [sp, #16]             // 16-byte Folded Spill
        stp     d11, d10, [sp, #32]             // 16-byte Folded Spill
        stp     d9, d8, [sp, #48]               // 16-byte Folded Spill
        str     x30, [sp, #64]                  // 8-byte Folded Spill
        str     s0, [sp, #76]                   // 4-byte Folded Spill
        smstart sm
        ldr     s0, [sp, #76]                   // 4-byte Folded Reload
        bl      bar
        str     s0, [sp, #76]                   // 4-byte Folded Spill
        smstop  sm
        ldp     d9, d8, [sp, #48]               // 16-byte Folded Reload
        ldp     d11, d10, [sp, #32]             // 16-byte Folded Reload
        ldp     d13, d12, [sp, #16]             // 16-byte Folded Reload
        ldr     s0, [sp, #76]                   // 4-byte Folded Reload
        ldr     x30, [sp, #64]                  // 8-byte Folded Reload
        ldp     d15, d14, [sp], #80             // 16-byte Folded Reload
        ret

在 ISD 節點上設置正確的暫存器遮罩並在正確的位置插入 smstart/smstop 應可確保正確完成此操作。

指令選擇節點

AArch64ISD::SMSTART Chain, [SM|ZA|Both], CurrentState, ExpectedState[, RegMask]
AArch64ISD::SMSTOP  Chain, [SM|ZA|Both], CurrentState, ExpectedState[, RegMask]

SMSTART/SMSTOP 節點採用 CurrentStateExpectedState 運算元來處理條件式 SMSTART/SMSTOP 的情況。 僅當 CurrentState != ExpectedState 時,才會執行指令。

CurrentStateExpectedState 可以在編譯時進行評估時(即它們都是常數),則會發出無條件的 smstart/smstop 指令。 否則,該節點將與擴展為比較/分支和 smstart/smstop 的虛擬指令相匹配。 這對於實現從 SC -> NSC -> S 的轉換是必要的。

未鏈接的函式呼叫

當具有「aarch64_pstate_sm_enabled」的函式呼叫與串流不相容的函式時,編譯器必須在呼叫之前插入 SMSTOP,並在呼叫之後插入 SMSTOP。

如果被呼叫的函式是沒有副作用的內建函式,而該內建函式又被降低為函式呼叫(例如 @llvm.cos()),則對 @llvm.cos() 的呼叫不屬於任何鏈;它可以自由排程。

呼叫端的降低會建立一個小的節點鏈,該鏈

  • 啟動呼叫序列

  • 將輸入值從虛擬暫存器複製到 ABI 指定的實體暫存器

  • 執行分支和連結

  • 停止呼叫序列

  • 將輸出值從其物理暫存器複製到虛擬暫存器

當未 使用呼叫端的鏈時,只會使用鏈式序列中的結果值,而鏈本身則會被捨棄。

雖然 ISD 節點 SMSTARTSMSTOP 會回傳一個鏈 (Chain),但沒有實際的值,因此當 SMSTART/SMSTOP 節點是不被使用的鏈的一部分時,這些節點就不會被考慮排程,並且會從 DAG 中移除。為了防止這些節點被移除,我們需要一種方法來確保只有在執行 SMSTART/SMSTOP **之後**才能使用 CopyFromReg 的結果。

我們可以使用 CopyToReg -> CopyFromReg 序列來達成此目的,它會將值移入/移出虛擬暫存器,並將這些節點與 SMSTART/SMSTOP 鏈接在一起,使其成為計算結果值的表達式的一部分。產生的 COPY 節點會由暫存器分配器移除。

以下範例顯示如何在不透過鏈而是透過值將結果連結在一起的 DAG 中使用此方法

            t0: ch,glue = AArch64ISD::SMSTOP ...
          t1: ch,glue = ISD::CALL ....
        t2: res,ch,glue = CopyFromReg t1, ...
      t3: ch,glue = AArch64ISD::SMSTART t2:1, ....   <- this is now part of the expression that returns the result value.
    t4: ch = CopyToReg t3, Register:f64 %vreg, t2
  t5: res,ch = CopyFromReg t4, Register:f64 %vreg
t6: res = FADD t5, t9

我們也需要在區域串流函式中使用此方法,其中需要在函式開始時將 SMSTART 插入 DAG 中。

具有 __attribute__((arm_locally_streaming)) 的函式

如果函式被標記為 arm_locally_streaming,則序言/結尾中的執行階段 SVE 向量長度可能與函式主體中的向量長度不同。發生這種情況是因為我們在設定堆疊框架後呼叫 smstart,並在釋放堆疊框架之前呼叫 smstop。

為了確保我們使用正確的 SVE 向量長度來配置區域變數,我們可以使用串流向量長度透過 ADDSVL 指令來配置堆疊位置,即使 CPU 尚未處於串流模式也是如此。

這僅適用於區域變數,而不適用於被呼叫者儲存位置,因為 LLVM 不支援在一個堆疊框架中混合使用兩種不同的可縮放向量長度。這意味著目前不支援函式被標記為 arm_locally_streaming 並且需要在序言中溢出 SVE 被呼叫者儲存位置的情況。但是,在沒有使用者介入的情況下,這不太可能發生,因為 arm_locally_streaming 函式無法接受或回傳與向量長度相關的值。否則,將需要強制使用 SVE PCS(使用「aarch64_sve_pcs」)並結合使用 arm_locally_streaming 才能遇到此問題。可以透過在 Clang 中發出診斷訊息來防止這種組合。

以下範例顯示具有 arm_locally_streaming 屬性的函式的序言/結尾應該是什麼樣子

#define N 64

void __attribute__((arm_streaming_compatible)) some_use(svfloat32_t *);

// Use a float argument type, to check the value isn't clobbered by smstart.
// Use a float return type to check the value isn't clobbered by smstop.
float __attribute__((noinline, arm_locally_streaming)) foo(float arg) {
  // Create local for SVE vector to check local is created with correct
  // size when not yet in streaming mode (ADDSVL).
  float array[N];
  svfloat32_t vector;

  some_use(&vector);
  svst1_f32(svptrue_b32(), &array[0], vector);
  return array[N - 1] + arg;
}

應該使用 ADDSVL 來配置堆疊空間,並且應該避免覆寫回傳值/引數值。

_Z3foof:                                // @_Z3foof
// %bb.0:                               // %entry
        stp     d15, d14, [sp, #-96]!           // 16-byte Folded Spill
        stp     d13, d12, [sp, #16]             // 16-byte Folded Spill
        stp     d11, d10, [sp, #32]             // 16-byte Folded Spill
        stp     d9, d8, [sp, #48]               // 16-byte Folded Spill
        stp     x29, x30, [sp, #64]             // 16-byte Folded Spill
        add     x29, sp, #64
        str     x28, [sp, #80]                  // 8-byte Folded Spill
        addsvl  sp, sp, #-1
        sub     sp, sp, #256
        str     s0, [x29, #28]                  // 4-byte Folded Spill
        smstart sm
        sub     x0, x29, #64
        addsvl  x0, x0, #-1
        bl      _Z10some_usePu13__SVFloat32_t
        sub     x8, x29, #64
        ptrue   p0.s
        ld1w    { z0.s }, p0/z, [x8, #-1, mul vl]
        ldr     s1, [x29, #28]                  // 4-byte Folded Reload
        st1w    { z0.s }, p0, [sp]
        ldr     s0, [sp, #252]
        fadd    s0, s0, s1
        str     s0, [x29, #28]                  // 4-byte Folded Spill
        smstop  sm
        ldr     s0, [x29, #28]                  // 4-byte Folded Reload
        addsvl  sp, sp, #1
        add     sp, sp, #256
        ldp     x29, x30, [sp, #64]             // 16-byte Folded Reload
        ldp     d9, d8, [sp, #48]               // 16-byte Folded Reload
        ldp     d11, d10, [sp, #32]             // 16-byte Folded Reload
        ldp     d13, d12, [sp, #16]             // 16-byte Folded Reload
        ldr     x28, [sp, #80]                  // 8-byte Folded Reload
        ldp     d15, d14, [sp], #96             // 16-byte Folded Reload
        ret

防止在串流模式中使用不合法的指令

  • 在串流模式 (PSTATE.SM=1) 下執行程式時,SVE/SVE2 指令的子集以及大多數 AdvSIMD/NEON 指令都是無效的。

  • 在一般模式 (PSTATE.SM=0) 下執行程式時,SME 指令的子集是無效的。

  • 與串流相容的函式只能使用在 PSTATE.SM=0 或 PSTATE.SM=1 時都有效的指令。

PSTATE.SM 的值不受特徵旗標控制,而是由函數屬性控制。這意味著我們可以編譯為「+sme」,並且編譯器將會產生任何指令的程式碼,即使它們在請求的串流模式下不合法。編譯器需要使用函數屬性來確保編譯器不會在某些操作在執行時可用的假設下進行轉換。

我們有意識地選擇不使用特徵旗標來建模這一點,因為我們仍然希望在任一模式下支援內嵌組譯 (使用者手動放置 smstart/smstop),並且由於 TableGen 的限制,在個別指令層級實作這一點變得相當複雜(請參閱 D120261D121208)。

作為第一步,這意味著當函數具有 aarch64_pstate_sm_enabledaarch64_pstate_sm_bodyaarch64_pstate_sm_compatible 屬性時,我們將完全停用向量化(LoopVectorize/SLP),以避免使用向量指令。

稍後,我們的目標是放寬這些限制,以便使用串流相容指令的子集實現可擴展的自動向量化,但这需要對 CostModel、Legalization 和 SelectionDAG 降低進行更改。

我們還將在 Clang 中發出診斷訊息,以防止在使用串流模式屬性裝飾函數時使用非串流(相容)操作,例如通過 ACLE 內建函數。

其他需要考慮的事項

  • 當呼叫站點需要切換 PSTATE.SM 或當被呼叫方的函數主體以与其呼叫方不同的串流模式執行時,必須停用內嵌。這是必需的,因為函數呼叫是串流模式更改的邊界。

  • 當呼叫站點需要切換 PSTATE.SM 時,必須停用尾部呼叫優化,以便呼叫方可以恢復 PSTATE.SM 的原始值。

3. 處理 PSTATE.ZA

與 PSTATE.SM 不同,啟用 PSTATE.ZA 不會影響 SVE 向量長度,也不會覆蓋 FP/AdvSIMD/SVE 寄存器。這意味著使用內建函數切換 PSTATE.ZA 是安全的。這也使得為私有 ZA 函數(即可能直接或間接覆蓋 ZA 狀態的函數)設置延遲保存機制變得更加簡單。

為了處理標記為 aarch64_new_za 的函數,我們引入了一個新的 LLVM IR 階段(SMEABIPass),該階段在 SelectionDAG 之前運行。任何由此階段處理的此類函數都標記為 aarch64_expanded_pstate_za

設置延遲保存

提交延遲保存

異常處理和 ZA

4. 類型

AArch64 謂詞作為計數器類型

概述:

謂詞作為計數器類型表示保存在 AArch64 SVE 謂詞寄存器中的謂詞作為計數器值的類型。此類值包含有關活動通道數、元素寬度以及指示是否應反轉生成的遮罩的位的資訊。應使用 ACLE 內建函數在謂詞向量之間移動謂詞作為計數器值。

該類型存在某些限制

  • 該類型可用於函數參數和返回值。

  • 此類型支援的 LLVM 操作僅限於 loadstorephiselectalloca 指令。

謂詞作為計數器類型是一種可擴展類型。

語法:

target("aarch64.svcount")

5. 參考文獻

  1. SME ACLE 拉取請求

  2. SME ABI 拉取請求