内存模型松弛注解

介绍

内存模型松弛注解 (MMRAs) 是目标定义指令的属性,可用于选择性地放宽内存模型施加的约束。例如

  • 在 SPIRV 程序中使用 VulkanMemoryModel 允许某些内存操作在 acquirerelease 操作之间重新排序。

  • OpenCL API 公开了仅 fencing 特定地址空间集合的原语。将该信息传递到后端可以启用更快的同步指令,而不是每次都 fencing 所有地址空间。

MMRAs 为目标提供了一种选择加入系统,以放宽默认的 LLVM 内存模型。因此,它们使用 LLVM 元数据附加到操作,这些元数据始终可以删除而不会影响正确性。

定义

内存操作

标记为访问内存的加载、存储、原子操作或函数调用。

同步操作

与其他线程同步内存的指令(例如原子操作或 fence)。

标签

附加到内存或同步操作的元数据,表示有关内存同步的某些目标定义属性。

一个操作可能具有多个标签,每个标签代表不同的属性。

标签由一对元数据字符串组成:前缀后缀

在 LLVM IR 中,这对字符串使用元数据元组表示。在其他情况下(注释、文档等),我们可能使用 prefix:suffix 表示法。例如

列表 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) 中位置的 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

MMRAs 可以支持 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 system-synchronizes-with 关系隐式同步。此示例假定 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 的操作应刷新或使缓存失效。

    • 指定两者或两者的操作应保守地刷新或使缓存失效以确保正确性。

注意

在这种实现中,删除操作上的元数据虽然不影响正确性,但可能会对性能产生重大影响。例如,操作在不应该绕过缓存时绕过了缓存。

内存类型

MMRAs 可以表达不同内存类型的选择性同步。

例如,目标可以公开 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,它可以采用显式地址空间进行 fencing。

默认情况下,LLVM 没有在 IR 中携带该信息的手段,因此该信息在降低到 LLVM IR 期间会丢失。这意味着诸如 AMDGPU 之类的目标必须保守地发出指令以在所有情况下 fencing 所有地址空间,这可能会对高性能应用程序的性能产生显着影响。

MMRAs 可用于在 IR 级别一直到代码生成级别保留该信息。例如,仅影响全局地址空间 addrspace(1) 的 fence 可以降低为

fence release # sync-as:1

并且目标可以使用 sync-as:1 的存在来推断它必须仅发出指令来 fencing 全局地址空间。

请注意,由于 MMRAs 是选择加入的,因此没有 MMRA 元数据的 fence 仍然可以保守地降低,因此此优化仅在前端在 fence 指令上发出 MMRA 元数据时才适用。

附加主题

注意

以下各节是信息性的。

性能影响

MMRAs 是捕获程序中优化机会的一种方式。但是,当操作未提及任何标签或冲突标签时,目标可能需要生成保守代码以确保正确性,但会牺牲性能。这可能在以下情况下发生

  1. 当目标首次引入 MMRAs 时,前端可能尚未更新以发出它们。

  2. 优化可能会删除 MMRA 元数据。

  3. 优化可能会向操作添加任意标签。

请注意,目标始终可以选择忽略(甚至删除)MMRAs 并恢复为默认行为/代码生成启发式方法,而不会影响正确性。

缺少先发生 的后果

先发生 部分中,我们定义了如何通过利用 MMRAs 之间的兼容性来打破两个指令之间的先发生 关系。当指令不兼容且没有先发生 关系时,我们说指令“不必相对于彼此排序”。

在这种上下文中,“排序”是一个非常广泛的术语,涵盖静态和运行时方面。

当没有排序约束时,如果重新排序不破坏其他约束(如单位置一致性),我们可以在优化器转换中静态地重新排序指令。静态重新排序是打破先发生 的后果之一,但不是最有趣的后果。

运行时后果更有趣。当指令之间存在先发生 关系时,目标必须发出同步代码以确保其他线程将以正确的顺序观察指令的效果。

例如,目标可能必须等待先前的加载和存储完成,然后才能启动 fence-release,或者可能需要在执行下一条指令之前刷新内存缓存。在缺少先发生 的情况下,没有这样的要求,也不需要等待或刷新。在某些情况下,这可能会显着加快执行速度。

组合操作

如果 pass 可以将多个内存或同步操作组合为一个操作,则它需要能够组合 MMRAs。实现此目的的一种可能方法是执行标签集的前缀式联合。

令 A 和 B 为两个标签集,U 为 A 和 B 的前缀式联合。对于 A 或 B 中存在的每个唯一标签前缀 P

  • 如果 A 或 B 都没有带有前缀 P 的标签,则不会向 U 添加带有前缀 P 的标签。

  • 如果 A 和 B 都至少有一个带有前缀 P 的标签,则来自两个集合的所有带有前缀 P 的标签都将添加到 U。

Pass 应避免积极地组合 MMRAs,因为这可能会导致大量信息丢失。虽然这不会影响正确性,但可能会影响性能。

作为一般经验法则,常见的 pass(如 SimplifyCFG)会积极地组合/重新排序操作,应仅组合具有相同标签集的指令。不太频繁组合或充分了解组合 MMRAs 成本的 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