記憶體模型鬆綁註解

簡介

記憶體模型鬆綁註解 (MMRA) 是指令上的目標定義屬性,可用於選擇性地放寬記憶體模型施加的限制。例如

  • 在 SPIRV 程式中使用 VulkanMemoryModel 允許某些記憶體操作在 acquirerelease 操作之間重新排序。

  • OpenCL API 公開了僅對一組特定位址空間進行 fence 的原語。將該資訊傳遞到後端可以啟用使用更快的同步指令,而不是每次都對所有位址空間進行 fence。

MMRA 為目標提供了一個選擇性加入系統,以放寬預設的 LLVM 記憶體模型。因此,它們使用 LLVM 中繼資料附加到操作,這些中繼資料始終可以在不影響正確性的情況下刪除。

定義

記憶體操作

標記為存取記憶體的載入、儲存、原子操作或函數呼叫。

同步操作

與其他執行緒同步記憶體的指令(例如原子操作或 fence)。

標籤

附加到記憶體或同步操作的中繼資料,表示與記憶體同步相關的一些目標定義屬性。

一個操作可能有多個標籤,每個標籤代表不同的屬性。

標籤由一對中繼資料字串組成:前綴後綴

在 LLVM IR 中,該對使用中繼資料元組表示。在其他情況下(註解、文件等),我們可以使用 前綴:後綴 符號。例如

清單 20 範例:中繼資料中的標籤
!0 = !{!"scope", !"workgroup"}  # scope:workgroup
!1 = !{!"scope", !"device"}     # scope:device
!2 = !{!"scope", !"system"}     # scope:system

備註

與最佳化程式相關的唯一語義是下面定義的「相容性」關係。所有其他語義均由目標定義。

標籤也可以組織在列表中,以允許操作指定它們所屬的所有標籤。此類列表稱為「標籤集」。

清單 21 範例:中繼資料中的標籤集
!0 = !{!"scope", !"workgroup"}
!1 = !{!"sync-as", !"private"}
!2 = !{!0, !2}

備註

如果操作沒有 MMRA 中繼資料,則將其視為具有空的標籤列表 (!{})。

請注意,如果一個標籤沒有被應用它的指令或當前目標所識別,這並不是一個錯誤。這樣的標籤會被直接忽略。

同步操作和記憶體操作都可以使用 !mmra 語法附加零個或多個標籤。

為了便於閱讀以下範例,我們使用一個(非功能性)的簡短語法來表示 MMMRA 中繼資料。

列表 22 簡短語法範例
store %ptr1 # foo:bar
store %ptr1 !mmra !{!"foo", !"bar"}

這兩種表示法可以在本文檔中使用,並且嚴格等效。但是,只有第二個版本具有功能性。

相容性

如果兩個標籤集合滿足以下條件,則稱它們是*相容的*:對於至少一個集合中存在的每個唯一標籤前綴 P

  • 另一個集合不包含帶有前綴 P 的標籤,或者

  • 兩個集合至少有一個共同的帶有前綴 P 的標籤。

上述定義意味著空集合始終與任何其他集合相容。這是一個重要的特性,因為它確保如果轉換刪除操作上的中繼資料,它永遠不會影響正確性。換句話說,無法透過從指令中刪除中繼資料來進一步放寬記憶體模型。

發生在…之前關係

相容性檢查可用於選擇退出在兩條指令之間建立的*發生在…之前*關係。

排序

當兩條指令的元數據不相容時,它們之間的任何程式順序都不在*發生在…之前*關係中。

例如,考慮目標公開的兩個標籤 foo:barfoo:baz

A: store %ptr1                 # foo:bar
B: store %ptr2                 # foo:baz
X: store atomic release %ptr3  # foo:bar

在上圖中,AX 相容,因此 A 發生在 X 之前。但 BX 不相容,因此它沒有發生在 X 之前。

同步

如果同步操作有一個或多個標籤,則它是否與其他操作同步以及是否參與 seq_cst 順序取決於目標。

以下範例是否與另一個序列同步取決於 foo:barfoo:bux 的目標定義語義。

fence release               # foo:bar
store atomic %ptr1          # foo:bux

範例

範例 1
A: store ptr addrspace(1) %ptr2                  # sync-as:1 vulkan:nonprivate
B: store atomic release ptr addrspace(1) %ptr3   # sync-as:0 vulkan:nonprivate

A 和 B 彼此之間沒有順序關係(沒有*發生在…之前*關係),因為它們的標籤集合不相容。

請注意,sync-as 值不必與 addrspace 值匹配。例如,在範例 1 中,對 addrspace(1) 中位置的釋放儲存只想與 addrspace(0) 中發生的操作同步。

範例 2
A: store ptr addrspace(1) %ptr2                 # sync-as:1 vulkan:nonprivate
B: store atomic release ptr addrspace(1) %ptr3  # sync-as:1 vulkan:nonprivate

A 和 B 的順序不受影響,因為它們的標籤集合相容。

請注意,由於其他原因,A 和 B 可能在也可能不在*發生在…之前*關係中。

範例 3
A: store ptr addrspace(1) %ptr2                 # sync-as:1 vulkan:nonprivate
B: store atomic release ptr addrspace(1) %ptr3  # vulkan:nonprivate

A 和 B 的順序不受影響,因為它們的標籤集合相容。

範例 4
A: store ptr addrspace(1) %ptr2                 # sync-as:1
B: store atomic release ptr addrspace(1) %ptr3  # sync-as:2

A 和 B 不必彼此相對排序(無「*先行發生*」關係),因為它們的標記集合不相容。

使用案例

SPIRV NonPrivatePointer

MMRA 可以支援 SPIRV 功能 VulkanMemoryModel,其中同步操作只會影響指定 NonPrivatePointer 語義的記憶體操作。

以下範例是使用以下方法從 SPIRV 程式產生的

  • vulkan:nonprivate 新增至每個同步操作。

  • vulkan:nonprivate 新增至標記為 NonPrivatePointer 的每個非原子記憶體操作。

  • vulkan:private 新增至未標記為 NonPrivatePointer 的每個非原子記憶體操作的標記。

Thread T1:
 A: store %ptr1                 # vulkan:nonprivate
 B: store %ptr2                 # vulkan:private
 X: store atomic release %ptr3  # vulkan:nonprivate

Thread T2:
 Y: load atomic acquire %ptr3   # vulkan:nonprivate
 C: load %ptr2                  # vulkan:private
 D: load %ptr1                  # vulkan:nonprivate

相容性確保操作 A 相對於 X 排序,而操作 D 相對於 Y 排序。如果 XY 同步,則 A 先於 D 發生。無法推斷操作 BC 之間的這種關係。

備註

Vulkan 記憶體模型 將所有原子操作視為非私有。

是否在原子操作上指定 vulkan:nonprivate 是一個實作細節,因為原子操作始終是「*非私有*」的。實作可以選擇明確地在每個原子操作上使用 vulkan:nonprivate 發出 IR,或者它可以選擇僅發出 vulkan::private 並預設為 vulkan:nonprivate

標記為 vulkan:private 的操作實際上選擇退出 SPIRV 程式中的先行發生順序,因為它們與每個同步操作都不相容。請注意,未標記為 NonPrivatePointer 的 SPIRV 操作並非完全私屬於執行緒 - 它們在執行緒的開始或結束時透過 Vulkan「*系統同步*」關係隱式同步。此範例假設 vulkan:private 的目標定義語義正確實作了此屬性。

此機制足以表達 SPIRV 程式與其他環境的互通性。

Thread T1:
A: store %ptr1                 # vulkan:nonprivate
X: store atomic release %ptr2  # vulkan:nonprivate

Thread T2:
Y: load atomic acquire %ptr2   # foo:bar
B: load %ptr1

在上述範例中,執行緒 T1 來自 SPIRV 程式,而執行緒 T2 來自非 SPIRV 程式。 X 是否可以與 Y 同步取決於目標定義。如果 XY 同步,則 A 先於 B 發生(因為 A/X 和 Y/B 相容)。

實作範例

考慮在一個目標設備上實作 SPIRV NonPrivatePointer,其中所有記憶體操作都被快取,並且整個快取在 releaseacquire 時分別被清空或失效。一種可能的方案是,在轉譯 SPIRV 程式時,標記為 NonPrivatePointer 的記憶體操作不應被快取,並且在 acquirerelease 操作期間不應觸碰快取內容。

這可以使用共用 vulkan: 前綴的標籤來實作,如下所示

  • 對於記憶體操作

    • 具有 vulkan:nonprivate 的操作應繞過快取。

    • 具有 vulkan:private 的操作應被快取。

    • 未指定或同時指定兩者的操作應保守地繞過快取以確保正確性。

  • 對於同步操作

    • 具有 vulkan:nonprivate 的操作不應清空或失效快取。

    • 具有 vulkan:private 的操作應清空或失效快取。

    • 未指定或同時指定兩者的操作應保守地清空或失效快取以確保正確性。

備註

在這樣的實作中,刪除操作上的中繼資料雖然不會影響正確性,但可能會對效能產生重大影響。例如,操作在不應該繞過快取時繞過了快取。

記憶體類型

MMRA 可以表達不同記憶體類型的選擇性同步。

例如,目標設備可能會公開一個 sync-as:<N> 標籤來傳遞有關哪些地址空間透過執行同步操作來同步的資訊。

備註

地址空間在這裡被用作一個常見的例子,但這個概念可以應用於其他「記憶體類型」。這裡的「記憶體類型」是什麼意思取決於目標設備。

# let 1 = global address space
# let 3 = local address space

Thread T1:
A: store %ptr1                                  # sync-as:1
B: store %ptr2                                  # sync-as:3
X: store atomic release ptr addrspace(0) %ptr3  # sync-as:3

Thread T2:
Y: load atomic acquire ptr addrspace(0) %ptr3   # sync-as:3
C: load %ptr2                                   # sync-as:3
D: load %ptr1                                   # sync-as:1

在上圖中,XY 是對 global 地址空間中某個位置的原子操作。如果 XY 同步,則在 local 地址空間中 B 發生在 C 之前。但對於操作 AD 來說,儘管它們是在 global 地址空間中某個位置執行的,但無法做出這樣的陳述。

實作範例:將地址空間資訊新增至柵欄

OpenCL C 等語言提供了柵欄操作,例如 atomic_work_item_fence,可以採用顯式地址空間進行柵欄。

根據預設,LLVM 沒有辦法在 IR 中攜帶該資訊,因此在降低到 LLVM IR 期間,該資訊會遺失。這意味著 AMDGPU 等目標設備必須在所有情況下都保守地發出指令以柵欄所有地址空間,這在高效能應用程式中可能會對效能產生顯著影響。

MMRA 可用於在 IR 層級保存該資訊,一直到程式碼生成。例如,一個只影響全域地址空間 addrspace(1) 的圍欄可以降級為

fence release # sync-as:1

目標可以使用 sync-as:1 的存在來推斷它必須只發出指令來圍欄全域地址空間。

請注意,由於 MMRA 是選擇性加入的,因此沒有 MMRA 中繼資料的圍欄仍然可以保守地降級,因此只有當前端在圍欄指令上發出 MMRA 中繼資料時,此最佳化才會生效。

其他主題

備註

以下章節僅供參考。

效能影響

MMRA 是一種捕捉程式中最佳化機會的方法。但是,當一個操作沒有提到任何標籤或衝突的標籤時,目標可能需要產生保守的程式碼,以確保正確性,但會犧牲效能。這可能發生在以下情況

  1. 當目標第一次引入 MMRA 時,前端可能還沒有更新以發出它們。

  2. 最佳化可能會刪除 MMRA 中繼資料。

  3. 最佳化可能會向操作添加任意標籤。

請注意,目標始終可以選擇忽略(甚至刪除)MMRA,並恢復到預設行為/程式碼生成啟發式,而不會影響正確性。

不存在 *happens-before* 的後果

happens-before章節中,我們定義了如何利用 MMRA 之間的相容性來破壞兩個指令之間的 *happens-before* 關係。當指令不相容且不存在 *happens-before* 關係時,我們說這些指令「不必彼此相對排序」。

在這種情況下,「排序」是一個非常廣泛的術語,涵蓋了靜態和運行時方面。

當沒有排序約束時,如果重新排序不會破壞其他約束(如單一位置一致性),我們*可以*在最佳化器轉換中靜態重新排序指令。靜態重新排序是破壞 *happens-before* 的一個後果,但不是最有趣的後果。

執行階段的後果更有趣。當指令之間存在 *happens-before* 關係時,目標必須發出同步程式碼,以確保其他執行緒以正確的順序觀察指令的效果。

例如,目標可能必須等待先前的載入和儲存完成,然後才能開始圍欄釋放,或者可能需要在執行下一條指令之前清空記憶體快取。在沒有 *happens-before* 的情況下,則沒有這樣的需求,也不需要等待或清空。在某些情況下,這可能會顯著加快執行速度。

組合操作

如果一個過程可以將多個記憶體或同步操作組合成一個,則它需要能夠組合 MMRA。實現這一點的一種可能方法是對標籤集進行前綴式聯集。

令 A 和 B 為兩個標籤集,U 為 A 和 B 的前綴式聯集。對於 A 或 B 中存在的每個唯一標籤前綴 P

  • 如果 A 或 B 沒有前綴為 P 的標籤,則不會將前綴為 P 的標籤添加到 U 中。

  • 如果 A 和 B 都至少有一個前綴為 P 的標籤,則會將兩個集合中所有前綴為 P 的標籤都添加到 U 中。

處理程序應避免積極地合併 MMRAs,因為這可能會導致大量信息丟失。雖然這不會影響正確性,但可能會影響性能。

一般而言,諸如 SimplifyCFG 之類會積極合併/重新排序操作的常見處理程序,應僅合併具有相同標籤集的指令。較不常合併或充分意識到合併 MMRAs 成本的處理程序可以使用上述基於前綴的聯集。

範例

A: store release %ptr1  # foo:x, foo:y, bar:x
B: store release %ptr2  # foo:x, bar:y

# Unique prefixes P = [foo, bar]
# "foo:x" is common to A and B so it's added to U.
# "bar:x" != "bar:y" so it's not added to U.
U: store release %ptr3  # foo:x
A: store release %ptr1  # foo:x, foo:y
B: store release %ptr2  # foo:x, bux:y

# Unique prefixes P = [foo, bux]
# "foo:x" is common to A and B so it's added to U.
# No tags have the prefix "bux" in A.
U: store release %ptr3  # foo:x
A: store release %ptr1
B: store release %ptr2  # foo:x, bar:y

# Unique prefixes P = [foo, bar]
# No tags with "foo" or "bar" in A, so no tags added.
U: store release %ptr3