Legalizer

這個過程會轉換泛型機器指令,使它們合法。

合法指令的定義如下

  • 可選取 — 目標稍後將能夠將其選取為目標特定的(非泛型)指令。這並不一定意味著 InstructionSelect 必須處理它。這僅意味著某些東西必須處理它。

  • 可以載入和儲存的 vreg 進行操作 — 如果需要,目標可以為每個 gvreg 運算元選取 G_LOAD/G_STORE

與 SelectionDAG 相反,這裡沒有合法化階段。特別是,「類型」和「操作」合法化不是分開的。

合法化是迭代的,並且所有狀態都包含在 GMIR 中。為了保持中間代碼的有效性,引入了以下指令

  • G_MERGE_VALUES — 將多個相同大小的寄存器串聯成一個更大的寄存器。

  • G_UNMERGE_VALUES — 從一個更大的寄存器中提取多個相同大小的寄存器。

  • G_EXTRACT — 從一個更大的寄存器中提取一個簡單寄存器(作為連續的位元序列)。

由於它們預計是合法化過程中的臨時副產品,因此它們在 Legalizer 階段結束時被組合在一起。如果還有剩餘的,則預計它們始終是可選取的,必要時可以使用載入和儲存。

指令的合法性只能取決於指令本身,而不能取決於使用該指令的任何上下文。但是,在確定指令不合法之後,允許使用指令的上下文來決定如何使指令合法化。例如,如果我們有一個 G_FOO 指令,其形式為

%1:_(s32) = G_CONSTANT i32 1
%2:_(s32) = G_FOO %0:_(s32), %1:_(s32)

則不可能說 G_FOO 是合法的,如果 %1 是一個值為 1G_CONSTANT。但是,以下

%2:_(s32) = G_FOO %0:_(s32), i32 1

可以說,當且僅當運算元 2 是一個值為 1 的立即數時,它是合法的,因為該信息完全包含在單個指令中。

API:LegalizerInfo

推薦的 [1] API 看起來像這樣

getActionDefinitionsBuilder({G_ADD, G_SUB, G_MUL, G_AND, G_OR, G_XOR, G_SHL})
    .legalFor({s32, s64, v2s32, v4s32, v2s64})
    .clampScalar(0, s32, s64)
    .widenScalarToNextPow2(0)
    .clampNumElements(0, v2s32, v4s32)
    .clampNumElements(0, v2s64, v2s64)
    .moreElementsToNextPow2(0);

並描述了一組規則,通過這些規則我們可以聲明指令合法或決定採取哪些措施使其更加合法。

這個規則集的核心是 LegalityQuery,它描述了指令。我們使用描述而不是指令來允許其他過程在不必創建指令的情況下確定合法性,並且還將謂詞可用的信息限制為可以安全依賴的信息。目前,用於確定合法性的謂詞可用的信息包含

  • 指令的操作碼

  • 每個類型索引的類型(請參閱 type0type1 等)

  • 每個 MachineMemOperand 的位元組大小和原子排序

備註

另一個值得研究的替代方案是將 API 推廣到使用 std::function 來表示動作,而不是使用明確的列舉標記(LegalWidenScalar 等),這些標記會指示它調用函數來實現動作。這將帶來一些好處,最顯著的是可以移除 Custom。

註腳

規則處理和宣告規則

getActionDefinitionsBuilder 函數會為給定的操作碼生成一個規則集,可以將規則添加到該規則集中。如果給定了多個操作碼,則它們將永久綁定到同一個規則集。規則集中的規則從上到下執行,如果指令由於規則而合法化,則將從頂部重新開始。如果規則集在沒有滿足任何規則的情況下耗盡,則認為它不受支持。

當它沒有宣告指令合法時,每次通過規則可能會要求將一種類型更改為另一種類型。有時這可能會導致多種類型發生變化,但我們盡可能避免這種情況,因為進行多項更改可能會導致難以避免無限迴圈,例如,縮窄一種類型會導致另一種類型過小,而擴展該類型會導致第一種類型過大。

一般來說,建議盡可能靠近規則頂部宣告指令合法,並盡可能將任何昂貴的規則放在底部。這有助於提高性能,因為測試合法性的頻率高於合法化,並且合法化可能需要多次通過規則。

作為具體示例,請考慮以下規則

getActionDefinitionsBuilder({G_ADD, G_SUB, G_MUL, G_AND, G_OR, G_XOR, G_SHL})
    .legalFor({s32, s64, v2s32, v4s32, v2s64})
    .clampScalar(0, s32, s64)
    .widenScalarToNextPow2(0);

和指令

%2:_(s7) = G_ADD %0:_(s7), %1:_(s7)

這不符合 .legalFor() 的謂詞,因為 s7 不是列出的類型之一,因此它會落到 .clampScalar()。它確實符合此規則的謂詞,因為該類型小於 s32,並且此規則指示合法化器將類型 0 更改為 s32。然後它從頂部重新開始。這次它確實滿足了 .legalFor(),結果輸出為

%3:_(s32) = G_ANYEXT %0:_(s7)
%4:_(s32) = G_ANYEXT %1:_(s7)
%5:_(s32) = G_ADD %3:_(s32), %4:_(s32)
%2:_(s7) = G_TRUNC %5:_(s32)

其中 G_ADD 是合法的,其他指令則由合法化器安排處理。

規則動作

有各種規則工廠可以將規則附加到規則集,但它們有一些共同的動作

  • legalIf()legalFor() 等會在滿足謂詞的情況下宣告指令合法。

  • narrowScalarIf()narrowScalarFor() 等會在滿足謂詞的情況下宣告指令非法,並指示將其中一種類型中的標量縮窄為特定類型會使其更合法。此動作同時支持標量和向量。

  • widenScalarIf()widenScalarFor() 等函式會在滿足條件時,將指令宣告為不合法,並指出將其中一個類型的純量擴展到特定類型會使其更合法。此動作支援純量和向量。

  • fewerElementsIf()fewerElementsFor() 等函式會在滿足條件時,將指令宣告為不合法,並指出將其中一個類型的向量元素數量減少到特定類型會使其更合法。此動作支援向量。

  • moreElementsIf()moreElementsFor() 等函式會在滿足條件時,將指令宣告為不合法,並指出將其中一個類型的向量元素數量增加到特定類型會使其更合法。此動作支援向量。

  • lowerIf()lowerFor() 等函式會在滿足條件時,將指令宣告為不合法,並指出將其替換為等效指令會使其更合法。每個操作碼對此動作的支援有所不同。這些函式可能會提供一個可選的 LegalizeMutation,其中包含要在不同類型中嘗試執行擴展的類型。

  • libcallIf()libcallFor() 等函式會在滿足條件時,將指令宣告為不合法,並指出將其替換為程式庫呼叫會使其更合法。每個操作碼對此動作的支援有所不同。

  • customIf()customFor() 等函式會在滿足條件時,將指令宣告為不合法,並指出後端開發人員將提供使其更合法的方法。

  • unsupportedIf()unsupportedFor() 等函式會在滿足條件時,將指令宣告為不合法,並指出沒有辦法使其合法,編譯器應失敗。

  • fallback() 會回退到舊的 API,並且應該只在從該 API 移植現有程式碼時使用。

規則謂詞

規則工廠也有一些共同的謂詞

  • legal()lower() 等函式始終為真。

  • legalIf()narrowScalarIf() 等函式在使用者提供的 LegalityPredicate 函式傳回 true 時為真。此謂詞可以存取 LegalityQuery 中的資訊來做出決策。使用者提供的謂詞也可以使用 all(P0, P1, ...) 組合。

  • legalFor()narrowScalarFor() 等方法會在類型符合給定類型集合中的一種類型時滿足。例如,.legalFor({s16, s32}) 宣告如果類型 0 是 s16 或 s32,則指令合法。通常會提供兩個和三個類型索引的其他版本。對於這些版本,所有類型索引一起考慮時必須與其中一個元組中的所有類型匹配。因此,.legalFor({{s16, s32}, {s32, s64}}) 只會接受 {s16, s32}{s32, s64},但不會接受 {s16, s64}

  • legalForTypesWithMemSize()narrowScalarForTypesWithMemSize() 等方法類似於 legalFor()narrowScalarFor() 等方法,但還要求 MachineMemOperand 在每個元組中都具有給定的大小。

  • legalForCartesianProduct()narrowScalarForCartesianProduct() 等方法會在每個類型索引與每個獨立集合中的一個元素匹配時滿足。因此,.legalForCartesianProduct({s16, s32}, {s32, s64}) 會接受 {s16, s32}{s16, s64}{s32, s32}{s32, s64}

複合規則

有一些複合規則適用於由上述工具構建的常見情況

  • widenScalarToNextPow2() 類似於 widenScalarIf(),但僅在類型大小(以位元為單位)不是 2 的冪次方時才滿足,並選擇下一個最大的 2 的冪次方作為目標類型。

  • minScalar() 類似於 widenScalarIf(),但僅在類型大小(以位元為單位)小於給定最小值時才滿足,並選擇最小值作為目標類型。類似地,還有一個用於最大值的 maxScalar() 和一個用於同時執行兩者的 clampScalar()

  • minScalarSameAs() 類似於 minScalar(),但最小值取自另一個類型索引。

  • moreElementsToNextMultiple() 類似於 moreElementsToNextPow2(),但基於 X 的倍數而不是 2 的冪次方。

最小規則集

GlobalISel 的合法化器在目標如何塑造其餘後端必須處理的 GMIR 方面具有很大的靈活性。但是,所有目標都必須滿足少數要求。

在討論最低要求之前,我們需要一些術語

生產者類型集

由至少一條合法指令產生的所有可能類型的並集。

消費者類型集

由至少一條合法指令使用的所有可能類型的並集。

這兩組通常是相同的,但無法保證。例如,無法使用 s64 但仍然能夠為一些特定指令產生 s64 的情況並不少見。

純量的最小規則

  • 對於來自生產者類型集的所有輸入和來自消費者類型集的所有較大輸出,G_ANYEXT 必須合法。

  • 對於來自生產者類型集的所有輸入和來自消費者類型集的所有較小輸出,G_TRUNC 必須合法。

G_ANYEXT 和 G_TRUNC 具有強制合法性,因為 GMIR 需要一種方法來連接不同類型大小的操作。它們通常很容易支持,因為 G_ANYEXT 不會定義額外位的值,而 G_TRUNC 會捨棄位。其他轉換可以使用一些需要進一步合法化的額外操作降低為 G_ANYEXT/G_TRUNC。例如,G_SEXT 可以降低為

%1 = G_ANYEXT %0
%2 = G_CONSTANT ...
%3 = G_SHL %1, %2
%4 = G_ASHR %3, %2

並且 G_CONSTANT/G_SHL/G_ASHR 可以進一步降低為其他操作或目標指令。同樣,G_FPEXT 沒有合法性要求,因為它可以降低為 G_ANYEXT 後跟一個目標指令。

G_MERGE_VALUES 和 G_UNMERGE_VALUES 沒有合法性要求,因為前者可以降低為 G_ANYEXT 和其他一些可合法化的指令,而後者可以降低為一些可合法化的指令後跟 G_TRUNC。

向量的最小合法性

在向量類型中,LLVM IR 中沒有定義任何轉換,因為向量通常通過重新解釋位或通過分解向量並將其重構為不同類型來轉換。因此,G_BITCAST 是唯一要考慮的操作。我們通常不要求它是合法的,因為它通常可以降低為 COPY(或者使用 replaceAllUses() 降低為空)。但是,在某些情況下,G_BITCAST 並非微不足道(例如,大端 MIPS MSA 和大端 ARM NEON 上的大端數據的小端向量,請參閱 _i_bitcast)。為了說明這一點,G_BITCAST 對於所有改變值中的位模式的類型組合都必須是合法的。

G_BUILD_VECTOR 或 G_BUILD_VECTOR_TRUNC 沒有合法性要求,因為這些可以通過以下方式處理:* 宣告它們合法。* 將它們標量化。* 將它們降低為 G_TRUNC+G_ANYEXT 和一些可合法化的指令。* 將它們降低為根據定義合法的目標指令。

相同的推理也允許 G_UNMERGE_VALUES 缺少對向量輸入的合法性要求。

指標的最小合法性

指標沒有最小規則,因為 G_INTTOPTR 和 G_PTRTOINT 可以通過合法化器從一個寄存器類別選擇到另一個寄存器類別的 COPY。

操作的最小合法性

G_ANYEXT、G_MERGE_VALUES、G_BITCAST、G_BUILD_VECTOR、G_BUILD_VECTOR_TRUNC、G_CONCAT_VECTORS、G_UNMERGE_VALUES、G_PTRTOINT 和 G_INTTOPTR 的規則已在上面說明。除了這些之外,以下操作還有要求

  • 至少一個 G_IMPLICIT_DEF 必須是合法的。這通常是微不足道的,因為它不需要選擇任何代碼。

  • 對於生產者和消費者類型集中的所有類型,G_PHI 必須是合法的。這通常是微不足道的,因為它不需要選擇任何代碼。

  • 至少一個 G_FRAME_INDEX 必須是合法的

  • 至少一個 G_BLOCK_ADDR 必須是合法的

您可能期望還有許多其他操作具有合法性要求,但它們可以降低為根據定義合法的目標指令。