記憶體模型鬆弛標註

簡介

記憶體模型鬆弛標註 (MMRA) 是目標定義的指令屬性,可用於選擇性地鬆弛記憶體模型所施加的約束。例如

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

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

MMRA 提供了一個選擇加入系統,讓目標可以鬆弛預設的 LLVM 記憶體模型。因此,它們使用 LLVM metadata 附加到操作上,而這些 metadata 始終可以被刪除,而不會影響正確性。

定義

記憶體操作

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

同步操作

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

標籤

附加到記憶體或同步操作的 Metadata,表示關於記憶體同步的某些目標定義屬性。

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

標籤由一對 Metadata 字串組成:前綴後綴

在 LLVM IR 中,這一對使用 Metadata tuple 表示。在其他情況下(註解、文件等),我們可以使用 prefix:suffix 標記法。例如

列表 20 範例:Metadata 中的標籤
!0 = !{!"scope", !"workgroup"}  # scope:workgroup
!1 = !{!"scope", !"device"}     # scope:device
!2 = !{!"scope", !"system"}     # scope:system

注意

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

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

列表 21 範例:Metadata 中的標籤集合
!0 = !{!"scope", !"workgroup"}
!1 = !{!"sync-as", !"private"}
!2 = !{!0, !2}

注意

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

請注意,如果標籤未被應用它的指令或當前目標識別,則不是錯誤。這些標籤會被簡單地忽略。

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

為了以下範例的可讀性,我們使用(非功能性的)簡短語法來表示 MMMRA Metadata

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

這兩種標記法可以在本文檔中使用,並且完全等效。但是,只有第二個版本是功能性的。

相容性

當且僅當對於每個至少在一個集合中存在的唯一標籤前綴 P,兩個標籤集合被稱為是相容的

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

  • 至少有一個帶有前綴 P 的標籤在兩個集合中都是通用的。

上述定義暗示空集合始終與任何其他集合相容。這是一個重要的屬性,因為它確保如果轉換刪除操作上的 Metadata,則永遠不會影響正確性。換句話說,透過從指令中刪除 Metadata,記憶體模型無法進一步鬆弛。

發生在之前 關係

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

排序

當兩個指令的 Metadata 不相容時,它們之間的任何程式順序都不在發生在之前關係中。

例如,考慮目標公開的兩個標籤 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) 中位置的 store-release 操作只想與在 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 是否會在原子操作上指定是一個實作細節,因為原子操作始終是 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 的操作應刷新或無效化快取。

    • 指定兩者皆非或兩者的操作應保守地刷新或無效化快取以確保正確性。

注意

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

記憶體類型

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 同步,則 Blocal 位址空間中發生在 C 之前。但是,對於操作 AD,無法做出這樣的陳述,儘管它們是在 global 位址空間中的位置上執行的。

實作範例:為 Fence 加入位址空間資訊

諸如 OpenCL C 之類的語言提供了 fence 操作,例如 atomic_work_item_fence,它可以採用顯式的位址空間來進行 fence 操作。

預設情況下,LLVM 沒有在 IR 中攜帶該資訊的手段,因此該資訊在降低到 LLVM IR 時會遺失。這意味著諸如 AMDGPU 之類的目標必須保守地發出指令,以便在所有情況下對所有位址空間進行 fence 操作,這可能會對高效能應用程式產生顯著的效能影響。

MMRA 可用於在 IR 層級保留該資訊,一直到程式碼生成。例如,僅影響全域位址空間 addrspace(1) 的 fence 操作可以降低為

fence release # sync-as:1

並且目標可以使用 sync-as:1 的存在來推斷它必須僅發出指令來對全域位址空間進行 fence 操作。

請注意,由於 MMRA 是選擇加入的,因此沒有 MMRA Metadata 的 fence 操作仍然可以保守地降低,因此此最佳化僅在前端在 fence 指令上發出 MMRA Metadata 時才適用。

其他主題

注意

以下章節僅供參考。

效能影響

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

  1. 當目標首次引入 MMRA 時,前端可能尚未更新以發出它們。

  2. 最佳化可能會刪除 MMRA Metadata。

  3. 最佳化可能會將任意標籤新增到操作。

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

發生在之前 關係缺失的後果

發生在之前 章節中,我們定義了如何透過利用 MMRA 之間的相容性來打破兩個指令之間的發生在之前關係。當指令不相容且沒有發生在之前關係時,我們說指令「不必相對於彼此排序」。

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

當沒有排序約束時,如果重新排序不破壞諸如單一位置一致性之類的其他約束,我們可以在最佳化器轉換中靜態地重新排序指令。靜態重新排序是打破發生在之前關係的後果之一,但不是最有趣的後果。

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

例如,目標可能必須等待先前的載入和儲存完成,然後才能開始 fence-release 操作,或者可能需要在執行下一個指令之前刷新記憶體快取。在沒有發生在之前關係的情況下,沒有這種要求,也不需要等待或刷新。這在某些情況下可能會顯著加快執行速度。

合併操作

如果 pass 可以將多個記憶體或同步操作合併為一個,則它需要能夠合併 MMRA。實現此目的的一種可能方法是對標籤集合進行前綴式聯集。

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

  • 如果 A 或 B 都沒有帶有前綴 P 的標籤,則不會將帶有前綴 P 的標籤新增到 U。

  • 如果 A 和 B 都至少有一個帶有前綴 P 的標籤,則來自兩個集合的所有帶有前綴 P 的標籤都會新增到 U。

Pass 應避免積極合併 MMRA,因為這可能會導致資訊的嚴重遺失。雖然這不會影響正確性,但可能會影響效能。

作為一般經驗法則,諸如 SimplifyCFG 之類的常見 pass,它們積極地合併/重新排序操作,應僅合併具有相同標籤集合的指令。不太頻繁合併或充分了解合併 MMRA 成本的 Pass 可以使用上述前綴式聯集。

範例

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