3.3 存储模型

OpenCL内存模型描述了OpenCL程序运行时OpenCL平台公开的内存的结构,内容和行为。该模型允许程序员在主机程序和多个内核实例执行时推断内存中的值。

OpenCL程序定义了一个上下文,该上下文包括主机,一个或多个设备,命令队列以及在上下文中公开的内存。考虑这种程序涉及的执行单位。主机程序作为一个或多个主机线程运行,该线程由主机上运行的操作系统管理(其详细信息在OpenCL之外定义)。在单个上下文中可能有多个设备,所有设备都可以访问OpenCL定义的内存对象。在单个设备上,多个工作组可以与可能重叠的内存更新并行执行。最后,在单个工作组中,多个工作项同时执行,并且可能对内存进行重叠更新。

内存模型必须精确定义从这些执行单元中的每一个看到的内存中的值如何交互,以便程序员可以推断OpenCL程序的正确性。我们分为四个部分来定义内存模型。

  • 内存区域:主机和共享上下文的设备可以看到的独特内存。

  • 内存对象:由OpenCL API定义的对象,以及由主机和设备进行管理的对象。

  • 共享虚拟内存:在上下文中公开给主机和设备的虚拟地址空间。

  • 一致性模型:定义多个执行单元从内存中加载数据以及约束内存操作顺序并定义同步关系的原子/栅栏操作时,将观察到哪些值的规则。

内存模型:基本内存区域

OpenCL中的内存分为两部分。

  • 主机内存:主机直接可用的内存。主机内存的详细行为在OpenCL之外定义。内存对象通过OpenCL API中的功能或共享的虚拟内存接口在主机和设备之间移动。

  • 设备内存:内存可直接用于在OpenCL设备上执行的内核。

设备内存由四个命名的地址空间或内存区域组成:

  • 全局内存:此内存区域允许对在上下文内任何设备上运行的所有工作组中的所有工作项进行读/写访问。工作项可以读取或写入内存对象的任何元素。根据设备的功能,可能会缓存对全局内存的读取和写入。

  • 常量内存:在内核实例执行期间保持不变的全局内存区域。主机分配并初始化放置在恒定内存中的内存对象。

  • 本地内存:工作组本地的内存区域。该内存区域可用于分配该工作组中所有工作项共享的变量。

  • 私有内存:工作项专用的内存区域。在一个工作项专用存储器中定义的变量对另一个工作项不可见。

内存区域及其与OpenCL平台模型的关系概述如下。本地和私有存储器始终与特定设备相关联。但是,全局和常量存储器在给定上下文中在所有设备之间共享。 OpenCL设备可能包括缓存,以支持对这些共享内存的有效访问。

要了解OpenCL中的内存,重要的是要了解这些命名地址空间之间的关系。设备可用的四个命名地址空间是不相交的,这意味着它们不会重叠。但是,这是一种逻辑关系,实现可以选择让这些不相交的命名地址空间共享物理内存。

程序员通常需要可从内核调用的函数,其中由这些函数操纵的指针可以指向多个命名的地址空间。这使程序员免于创建函数的多个副本的容易出错和浪费的实践。每个命名的地址空间一个。因此,全局,本地和专用地址空间属于单个通用地址空间。这是在嵌入式C标准(ISO / IEC 9899:1999)中使用的通用地址空间的概念之后进行紧密建模的。由于它们都属于单个通用地址空间,因此支持以下属性来指向设备内存中已命名地址空间的指针:

  • 指向通用地址空间的指针可以转换为指向全局,本地或私有地址空间的指针

  • 指向全局,本地或私有地址空间的指针可以转换为指向通用地址空间的指针。

  • 指向全局,本地或私有地址空间的指针可以隐式转换为指向通用地址空间的指针,但不允许相反。

常量地址空间与通用地址空间不相交。 与全局内存中的内存对象关联的内存地址未在内核实例之间,设备与主机之间以及设备之间保留。在这方面,全局存储器充当存储器对象的全局池,而不是地址空间。使用共享虚拟内存(SVM)时,可以放宽此限制。

SVM导致主机和上下文中所有设备之间的地址有意义,因此支持在OpenCL内核中使用基于指针的数据结构。它在逻辑上将全局内存的一部分扩展到主机地址空间中,从而使工作项可以访问主机地址空间。在具有对主机和一个或多个设备之间的共享地址空间的硬件支持的平台上,SVM还可以提供一种更有效的方式来在设备和主机之间共享数据。有关SVM的详细信息,请参见共享虚拟内存。

pic4 图4. OpenCL平台中公开的命名地址空间。全局和常量内存在上下文中的一个或多个设备之间共享,而本地和私有内存与单个设备相关联。每个设备可以包括一个可选的高速缓存,以支持对其全局和恒定地址空间视图的有效访问。

程序员可以使用内存一致性模型的功能来管理可能在一个或多个设备上运行的多个工作项对全局内存的安全访问。另外,在使用共享虚拟内存(SVM)时,内存一致性模型也可以用于确保主机线程安全地访问共享内存区域中的内存位置。

3.3.2 内存模型:内存对象

全局内存的内容是内存对象。内存对象是全局内存的引用计数区域的句柄。内存对象使用OpenCL类型cl_mem并分为三个不同的类。

  • 缓冲区:存储为连续内存块的内存对象,用作通用对象来保存OpenCL程序中使用的数据。缓冲区内的值的类型可以是任何内置类型(例如int,float),向量类型或用户定义的结构。可以通过指针操纵缓冲区,就像使用C中的任何内存块一样。

  • 图像:图像存储对象保存一维,二维或三维图像。这些格式基于图形应用程序中使用的标准图像格式。图像是由OpenCL API中定义的函数管理的不透明数据结构。为了优化存储在许多GPU中的纹理存储器中存储的图像的操作,传统上不允许OpenCL内核读取和写入单个图像。但是,在OpenCL 2.0中,我们通过提供同步和隔离操作放松了这一限制,使程序员可以正确地同步其代码以安全地允许内核读取和写入单个映像。

  • 管道:管道存储对象从概念上讲是数据项的有序序列。管道具有两个端点:一个插入端点,数据项插入到该端点;一个读取端点,数据项从中删除。在任何时候,只有一个内核实例可以写入管道,并且只有一个内核实例可以从管道读取。为了支持生产者使用者设计模式,一个内核实例连接到写入端点(生产者),而另一个内核实例连接到读取端点(使用者)。

内存对象由主机API分配。主机程序可以为运行时提供指向连续内存块的指针,以在创建对象时保存内存对象(CL_MEM_USE_HOST_PTR)。或者,物理内存可以由OpenCL运行时管理,并且不能直接由主机程序访问。

在设备上运行的主机和工作项之间,对不同内存区域内的内存对象的分配和访问有所不同。这在“内存区域”表中进行了汇总,该表描述了内核还是主机可以从内存区域进行分配,分配的类型(编译时为静态,而运行时为动态)以及允许的访问类型(即,内核是否或主机可以读取和/或写入存储区域)。

表1.内存区域:

区域

全局

常量

本地

私有

Host

动态分配

动态分配

动态分配

不分配

Host

读/写(对缓冲区和图像的访问,但不对管道的访问)

kernel

程序范围变量的静态分配

静态分配

静态分配。 子内核动态分配

静态分配

kernel

读/写访问

只读访问

读/写访问。 无法访问孩子的本地存储。

读/写访问

一旦分配,就可以使一个内存对象可用于一个或多个设备上运行的内核实例。除了共享虚拟内存,还有三种基本方法可以管理主机和设备之间的缓冲区内容。

  • 读/写/填充命令:使用排队到OpenCL命令队列的命令在主机和全局内存区域之间显式读取和写入与内存对象关联的数据。

  • 映射/取消映射命令:来自内存对象的数据被映射到通过主机可访问指针访问的连续内存块中。在宿主程序可以安全地对其进行操纵之前,宿主程序先将映射命令加入到存储对象块中。当主机程序完成对内存块的处理后,主机程序将使用unmap命令,以允许内核实例安全地读取和/或写入缓冲区。

  • 复制命令:与内存对象关联的数据在两个缓冲区之间复制,每个缓冲区可以驻留在主机上或设备上。

使用读/写/映射,命令可以是阻塞操作或非阻塞操作。命令(内存传输)完成后,将返回用于阻止内存传输的OpenCL函数调用。此时,可以安全地重用主机上关联的内存资源,并确保主机上的后续操作已完成传输。对于无阻塞的内存传输,命令入队后,OpenCL函数调用将立即返回。

内存对象绑定到上下文,因此可以出现在多个物理设备上运行的多个内核实例中。 OpenCL平台必须支持广泛的硬件平台,包括不支持硬件中单个共享地址空间的系统。因此限制了内核实例之间共享内存对象的方式。基本原理是,允许在时间上重叠的多个内核实例对内存对象进行多次读取操作,但是仅当与Shared Virtual一起使用细粒度的同步时,才允许将重叠的读写操作从不同的内核实例混合到同一内存对象记忆。

当全局内存由在多个设备上运行的多个内核实例操纵时,OpenCL运行时系统必须管理内存对象与给定设备的关联。在大多数情况下,OpenCL运行时会将内存对象与设备隐式关联。内核实例自然与提交内核的命令队列相关联。由于命令队列只能访问单个设备,因此队列唯一地定义了任何给定的内核实例涉及哪个设备。因此在内存对象,内核实例和设备之间定义了明确的关联。程序员可以在他们的程序中预期这些关联,并显式管理内存对象与设备的关联以提高性能。

3.3.3。内存模型:共享虚拟内存

OpenCL通过共享虚拟内存(SVM)机制将全局内存区域扩展到主机内存区域。 OpenCL中有三种类型的SVM

  • 粗粒度缓冲区SVM:共享发生在OpenCL缓冲区内存对象区域的粒度上。在同步点和使用map / unmap命令强制一致性,以驱动主机和设备之间的更新。这种SVM形式类似于非SVM的内存使用方式。但是,它允许内核实例与主机程序共享基于指针的数据结构(例如,链表)。程序范围的全局变量被视为按设备的粗粒度SVM,以进行寻址和共享。

  • 细粒度的缓冲区SVM:共享发生在OpenCL缓冲区内存对象内单个加载/存储到字节的粒度上。加载和存储可能会被缓存。这意味着可以确保同步点的一致性。如果支持可选的OpenCL原子,则可以使用它们来提供对内存一致性的细粒度控制。

  • 精细的系统SVM:共享发生在单个加载/存储的粒度上,发生在主机内存中任何位置的字节中。可以缓存加载和存储,因此可以确保同步点的一致性。如果支持可选的OpenCL原子,则可以使用它们来提供对内存一致性的细粒度控制。

表2. OpenCL中的共享虚拟内存(SVM)选项摘要

SVM

共享的粒度

内存分配

实施一致性的机制

主机与设备之间的显式更新

非SVM缓冲区

OpenCL内存对象(缓冲区)

clCreateBuffer

主机同步点位于同一设备上或设备之间。

是的,通过Map和Unmap命令。

粗粒度缓冲区SVM

OpenCL内存对象(缓冲区)

clSVMAlloc

设备之间的主机同步点

是的,通过Map和Unmap命令。

细粒度的缓冲区SVM

OpenCL内存对象(缓冲区)中的字节

clSVMAlloc

同步点加原子(如果支持)

No

细粒度系统SVM

主机内存(系统)中的字节

主机内存分配机制(例如malloc)

同步点加原子(如果支持)

No

核心OpenCL规范中需要粗粒度缓冲区SVM。两种更精细的方法是OpenCL中的可选功能。上面总结了从与内核实例相关的工作项访问主机内存的各种SVM机制。

3.3.4 内存模型:内存一致性模型

OpenCL内存模型告诉程序员他们对OpenCL实现的期望。保证哪些内存操作以什么顺序发生以及每个读取操作将返回哪些内存值。内存模型告诉编译器编写者在实现编译器优化时必须遵循哪些限制;它们可以将哪些变量缓存在寄存器中,以及何时可以围绕屏障或原子操作移动读取或写入。内存模型还告诉硬件设计人员有关硬件优化的限制。例如,当它们必须刷新或使硬件缓存无效时。

OpenCL中的内存一致性模型基于ISO C11编程语言中的内存模型。为了使演示文稿更精确,更完整,我们引用了ISO C11国际标准中的所有修改段落。从C11标准中选取或修改段落时,该段落及其在C11标准中的原始位置也将被标识。

对程序员来说,最直观的模型 顺序一致性 内存模型。 顺序一致性上面的步骤执行的每个单元 的执行。 每个访问内存位置看到的最后一个任务的位置 交叉。 同时为一个程序员顺序一致性是相对简单的 原因,实现顺序一致性是昂贵的。 因此,OpenCL实现轻松内存一致性模型; 即它是 可以编写程序,从内存加载违反顺序 一致性。 幸运的是,如果一个程序不包含任何种族和如果程序 使用原子操作,利用顺序记忆顺序一致 (OpenCL的默认内存排序),OpenCL程序执行 与顺序一致性。

程序员可以在一定程度上控制内存模型是如何放松 选择记忆订单同步操作。 同步和内存的精确语义正式订单 中定义的 内存排序规则 . 在这里,我们提供一个高水平的描述如何将这些记忆订单申请 原子的原子操作对象之间共享单位执行。 OpenCL memory_order选择是基于ISO C11标准 内存模型。 他们在某些指定OpenCL通过以下功能 枚举常量:

  • memory_order_relaxed :意味着没有约束。 这个内存顺序可以安全地用于增量反驳说 同时增加,但doesna€™t的保证 要求对操作其他的内存位置。 它也可以被使用,例如,分配和由专家做票 程序员实现无锁定算法。

  • memory_order_acquire :一个同步操作(栅栏或原子) 已获得语义“获得”的副作用 与它同步操作:如果一个获得同步的 版本,执行会看到所有的收购单位的副作用 前释放(可能还有后续副作用。) 作为 精心设计的协议,程序员可以使用一个“收购” 安全观察执行另一个单位的工作。

  • memory_order_release :一个同步操作(栅栏或原子 操作),释放语义“释放”副作用 获得同步操作。 所有副作用包括之前释放。 精心设计的协议的一部分,程序员可以使用一个“发布” 改变一个单位的制造执行可见到其他单位 执行。

  • memory_order_acq_rel :与acquire-release同步操作 语义属性的获取和释放内存 订单。 它通常用于顺序读-修改-写操作。

  • memory_order_seq_cst :每个单元的加载和存储执行 似乎在程序(即执行。 测序)秩序,和 加载和存储来自不同单位的执行似乎简单 交叉。

    无论指定memory_order,解决限制 内存操作跨异构平台增加相当大的开销 程序执行的。 OpenCL平台可以优化取决于特定的操作 内存一致性模型的功能限制的范围 内存操作。 不同的内存范围由memory_scope的值定义的 枚举常量:

  • memory_scope_work_item :只有在内存排序约束 的工作项 1 .

  • memory_scope_sub_group :只有在内存排序约束 子群。

  • memory_scope_work_group 只适用于:内存排序约束 工作项执行在一个“workgroup”。

  • memory_scope_device: 只适用于内存排序约束 工作项执行在一个设备上

  • memoryscope_all_svm_devices 内存排序约束适用于跨多个设备和主机(在使用SVM时)执行的工作项。使用memory_scope_all_svm_devices对没有CL​MEM​SVM​ATOMICS设置标志的缓冲区执行的释放将至少实现

这些内存范围定义的层次结构分析时可见性 排序操作的内存限制。 例如,如果一个程序员知道内存操作的序列 只是从一个“workgroup”相关的工作项的集合 (因此将运行在单个设备),实现的 管理内存的开销在其他设备在同一个订单 上下文。 这可以大大减少程序的开销。 所有内存范围使用时是有效的全局内存或本地内存。 对于本地内存,所有的可见性是在一个给定的“workgroup”约束 和范围更广泛的比 memory_scope_work_group 没有额外的意义。

下面(之前 OpenCL 框架 ),我们将解释同步结构和详细 规则需要使用OpenCLa€™年代轻松记忆模型。 感激是很重要的,然而,许多项目不受益 从轻松的记忆模型。 甚至专家程序员很难利用原子和栅栏 编写正确的程序与轻松记忆模型。 可以编写大量OpenCL的程序使用一个简化的内存 模型。 这是遵循这些指导方针。

  • 编写程序,管理安全的共享全局内存对象 定义的同步点命令队列。

  • 限制低水平同步内部工作组“workgroup” 等功能障碍。

  • 如果你想要顺序一致性和系统配置或行为 精密SVM与原子缓冲区支持,只使用 memory_order_seq_cst 操作的范围 memory_scope_all_svm_devices .

  • 如果你想要当不使用系统顺序一致性的行为 分配或精密SVM缓冲区在原子的支持下,只使用 memory_order_seq_cst 操作的范围 memory_scope_device 或 memory_scope_all_svm_devices .

  • 确保你的程序没有比赛。

如果遵循这些指导方针OpenCL的项目,你可以跳过 详细规则背后的轻松的内存模型和直接 OpenCL框架 .

3.3.5。 内存模型:原子和栅栏操作的概述

3.3.5。内存模型:原子和篱笆操作概述

OpenCL 2.0规范定义了许多同步操作,这些操作用于定义程序中的内存顺序约束。它们在控制如何使一个执行单元中的内存操作(例如工作项或使用SVM的主机线程)对另一执行单元可见时发挥特殊作用。OpenCL中有两种类型的同步操作:原子操作和围栏。

原子操作是不可分割的。它们要么完全发生,要么根本不发生。这些操作用于对执行单元之间的内存操作进行排序,因此它们使用由OpenCL内存一致性模型定义的memory_order和memory_scope参数进行参数化。OpenCL内核语言的原子操作类似于C11标准定义的相应操作。

OpenCL 2.0原子操作适用于原子类型(C11标准中的子集)的变量,包括int,uint,long,ulong,float,double,half,intptr_t,uintptr_t,size_t和ptrdiff_t类型的原子版本。但是,对其中一些原子类型的支持取决于对相应常规类型的支持。

一个或多个存储位置上的原子操作要么是获取操作,释放操作,要么是获取和释放操作。没有关联存储位置的原子操作是围栏,可以是获取围栏,发布围栏,或者是获取围栏和发布围栏。此外,还有不具有同步属性的宽松原子操作和具有特殊特征的原子读取-修改-写入操作。 [C11标准,第5.1.2.4节,第5段,已修改。]

The orders memory_order_acquire (used for reads), memory_order_release (used for writes), and memory_order_acq_rel(用于读-修改-写操作)用于使用共享变量在执行单元之间进行简单通信。非正式地,在原子对象A上执行memory_order_release使所有先前的副作用对于以后在A上执行memory_order_acquire的任何执行单元可见。order memory_order_acquire,memory_order_release和 memory_order_acq_rel不会为无竞争程序提供顺序一致性,因为它们会不能确保其他线程按此顺序看到原子存储和原子负载。

隔离操作是atomic_work_item_fence,它包括一个memory_order参数以及memory_scope和cl_mem_fence_flags参数。根据memory_order参数,此操作:

  • 果memory_order_relaxed没有影响;

  • 如果memory_order_acquire是获取栅栏;

  • 如果为memory_order_release,则为释放栅栏;

  • 如果memory_order_acq_rel,则既是获取隔离区,也是释放隔离区;

  • 如果具有memory_order_seq_cst,则是具有获取和释放语义的顺序一致的篱笆。

如果指定,cl_mem_fence_flags参数必须为CLK_IMAGE_MEM_FENCE,CLK_GLOBAL_MEM_FENCE,CLK_LOCAL_MEM_FENCE或CLK_GLOBAL_MEM_FENCE |。CLK_LOCAL_MEM_FENCE。

必须使用atomic_work_item_fence(CLK_IMAGE_MEM_FENCE)内置函数来确保无采样器的写入对于同一工作项以后的读取可见。如果不使用atomic_work_item_fence函数,就不能保证图像对象的读写一致性:如果工作项从之前已写入的图像中读取而没有中间的atomic_work_item_fence,则不能保证先前的写入对对象可见。工作项目。

OpenCL中的同步操作可以由memory_scope参数化。内存范围控制相对于内存模型可见的原子操作或篱笆的范围。在全局内存和本地内存上执行原子操作和防护时,可以使用这些内存作用域。在全局内存上使用时,可见性受该内存的功能限制。当在细粒度的非原子SVM缓冲区,粗粒度SVM缓冲区或非SVM缓冲区上使用时,使用memory_scope_all_svm_devices参数化的操作的 行为就好像是使用memory_scope_device参数化的操作一样。当在本地内存上使用时,可见性受工作组的限制,因此,具有比memory_scope_work_group更大的可见性的memory_scope 将减少到memory_scope_work_group。

如果两个动作A和B具有相同的范围P,则它们被定义为包含一个范围,使得:

  • 是memory_scope_sub_group,而A和B由同一子组中的工作项执行。

  • P是memory_scope_work_group,A和B由同一工作组内的工作项执行。

  • P是memory_scope_device和一个和乙是由同一个设备上工作项目时执行一个和乙应用到SVM分配或一个 和乙是由工作内容要在同一内核中执行,或者当它的一个子一个和乙申请到cl_mem缓冲区。

  • 如果A和B由主机线程或一个或多个可以彼此共享SVM内存和主机进程的设备上的工作项执行,则P为memory_scope_all_svm_devices。

3.3.6。内存模型:内存排序规则

从根本上讲,内存模型中的问题是要了解对内存中对象进行修改的时间顺序。修改对象或调用修改对象的函数是副作用,即执行环境状态的更改。对表达式的评估通常包括值计算和副作用的启动。左值表达式的值计算包括确定指定对象的身份。 [C11标准,第5.1.2.3节,第2段,已修改。]

我们假定OpenCL内核语言和主机编程语言在单个执行单元执行的评估之间具有先后顺序关系。这种先验顺序关系是这些评估之间的不对称,可传递,成对关系,从而在评估之间引起部分排序。给定任意两个评价一个和乙,如果一个被测序-之前乙,然后执行一个应先执行乙。(相反,如果A在B之前排序,则B在A之后排序 。)如果A在B之前不排序或在之后B,则A和 B是无序列的。当A在B之前或之后进行排序时,评估A和B的顺序不确定,但不确定。 [C11标准,第5.1.2.3节,第3段,已修改。]

在OpenCL内核语言中,工作项W在特定点可见的对象的值是该对象的初始值,W在对象中存储的值或另一工作在对象中存储的值-项目或宿主线程,请遵循以下规则。根据宿主编程语言的详细信息,宿主线程可见的对象的值也可以是另一个工作项或宿主线程存储在该对象中的值。 [C11标准,第5.1.2.4节,第2段,已修改。]

如果两个表达式求值之一修改一个内存位置,而另一个表达式读取或修改了相同的内存位置,则这两个表达式求值会发生冲突。 [C11标准,第5.1.2.4节,第4段。]

对特定原子对象M的所有修改都以某种特定的总顺序发生,称为M的修改顺序。如果A和B是原子对象M的变体,并且A 发生在B之前,则A应当按M的变体顺序 在B之前,其定义如下。请注意,一个原子对象的修改次序中号是独立的是否中号是在本地或全局存储器。 [C11标准,第5.1.2.4节,第7段,已修改。]

释放序列以对原子对象 M的释放操作A开始,并且是副作用的最大连续子序列(按M的修改顺序),其中第一个操作为A,随后的每个操作均由相同的工作执行-执行释放的项或宿主线程,或者是原子的读-修改-写操作。 [C11标准,第5.1.2.4节,第10段,已修改。]

OpenCL的本地和全局内存是不相交的。内核可以访问两种内存,而主机线程只能访问全局内存。此外,OpenCLs work_group_barrier函数的flags参数指定该函数将使哪些存储器操作可见:例如,这些存储器操作可以是本地存储器的存储器操作,全局存储器的存储器操作或两者。由于可以为本地内存和全局内存分别指定内存操作的可见性,因此我们定义了两个相关但独立的关系,全局同步与和本地同步与。全局内存上的某些操作可以与另一个工作项或主机线程执行的其他操作进行全局同步。一个示例是一个工作项中的发布原子操作,该工作项与第二个工作项中的获取原子操作进行全局同步。同样,内核中对本地对象的某些原子操作可以与这些本地对象上的其他原子操作进行本地同步。 [C11标准,第5.1.2.4节,第11段,已修改。]

我们定义了两个单独的事前关联:全局事前关联和本地事前关联。

全局内存动作A全局发生在全局内存动作B之前, 如果

  • 在B之前排序,或者

  • 一个全球进行同步,与乙,或

  • 对于某些全局记忆动作C,A在C之前 全局,C在B之前全局全局。

本地内存操作A本地发生在本地内存操作B之前,如果

  • 甲之前进行测序乙乙 ,或,或

  • 甲本地进行同步-与乙,或

  • 对于某些局部记忆操作C,A在C之前发生在 局部,C在B之前发生在局部。

OpenCL实现应确保没有程序执行在本地先于关系或全局先于关系中显示循环。

甲可见副作用甲上的全局对象中号相对于一个值计算乙的中号满足条件:

  • 个全球性的之前发生乙,和

  • 有其他副作用X到中号,使得甲 全局之前发生X和X全局之前发生乙。

我们类似地为局部对象M定义了可见的副作用。由评估B确定 的非原子标量对象M的值应为可见副作用A所存储的值。 [C11标准,第5.1.2.4节,第19段,已修改。]

如果程序的执行单元包含两个相互冲突的动作A和B,并且执行单元不同,则该程序的执行将引起数据争用,并且

  • (1)A或B中的至少一个不是原子的,或者A和B没有包含性的内存范围,并且

  • (2)动作是全局-先-后关系无序的全局动作,或者是局部-先-后关系无序的局部动作。

任何此类数据争用都会导致未定义的行为。 [C11标准,第5.1.2.4节,第25段,已修改。]

我们还定义了局部和全局原子对象上可见的副作用序列。本小节的其余段落为全局原子对象M定义了此序列;通过使用local-happens-before关系类似地定义了局部原子对象的可见副作用序列。

的副作用,在全球原子对象的可见序列中号,对于一个值计算乙的中号,是副作用的修饰顺序的最大连续子序列中号,其中第一副作用是相对于可见光B,并且对于每个副作用,都不是B在全局之前发生。由评估B确定的M的值应是通过某些操作相对于B在M的可见序列中存储的值。 [C11标准,第5.1.2.4节,第22段,已修改。]

如果操作一个用于修改的原子对象中号的操作之前全局发生乙修饰中号,然后甲应早于乙中的修改次序中号。此要求称为写-写一致性。

如果一个值计算一个原子对象的中号全局之前发生的值计算乙的中号,和甲取其值从副作用X 上中号,然后通过计算出的值乙应由等于存储在由值X,或者是由副作用存储的值ÿ上中号,其中ÿ 如下X中的修饰顺序中号。此要求称为读-读一致性。 [C11标准,第5.1.2.4节,第22段,已修改。]

如果原子对象M的值计算A全局发生在对M的操作B之前,则A应从 对M的副作用X中获取其值,其中X以M的修改顺序在B之前。此要求称为读写一致性。

如果副作用X上的原子对象中号全局之前发生的值计算乙的中号,然后评价乙应采取从其值 X或从副作用ý下面X中的修饰顺序 中号。该要求被称为读写一致性。

内存排序规则:原子操作

本节和以下各节描述内核C代码和宿主程序中不同的程序动作如何导致局部和全局发生之前的关系。本节讨论OpenCL 2.0原子操作的排序规则。

设备端队列定义了枚举类型的memory_order。

  • 对于memory_order_relaxed,没有操作对内存进行排序。

  • 对于memory_order_release,memory_order_acq_rel和 memory_order_seq_cst,存储操作对受影响的内存位置执行释放操作。

  • 对于memory_order_acquire,memory_order_acq_rel和 memory_order_seq_cst,装入操作对受影响的内存位置执行获取操作。 [C11标准,第7.17.3节,第2-4段,已修改。]

某些内置函数与另一个执行单元执行的其他内置函数同步。对于特定情况下的成对发布和获取操作,这是正确的。在全局对象M上执行释放操作 的原子操作A与在M上执行获取操作并读取以A开头的释放序列中任何副作用写入的值的原子操作B进行全局同步。类似的规则适用于对本地内存中的对象进行原子操作:原子操作A对本地对象M执行释放操作, 并与原子操作B本地同步它对M执行获取操作,并读取以A开头的释放序列中任何副作用写入的值。 [C11标准,第5.1.2.4节,第11段,已修改。]

所有memory_order_seq_cst 操作应存在一个总顺序S,该顺序应与所有受影响位置的修改顺序一致,以及这些位置的适当的全局先行和本地先行顺序,以便每个 memory_order_seq操作从全局或本地内存中的原子对象M加载值的 B观察到以下值之一:

  • 最后修改的结果阿的中号先于乙在小号,如果它存在,或

  • 如果存在A,则在对B的可见的副作用序列中M的某些修改结果不是 memory_order_seq_cst,并且不在A之前发生,或者

  • 如果A不存在,则在相对于B的可见可见副作用序列中对M进行某种修改的结果,即不是 memory_order_seq_cst。 [C11标准,第7.17.3节,第6段,已修改。]

    令X和Y为两个memory_order_seq_cst操作。如果X与Y本地同步或全局与Y同步,则X既与Y本地同步,又与Y全局同步。

如果总订单S存在,则以下规则成立:

  • For an atomic operation B that reads the value of an atomic object M,如果在B之前有一个memory_order_seq_cst栅栏X排序 ,则B观察到M在总顺序S中在X之前 对M的最后memory_order_seq_cst修改,或者在其修改顺序中观察M的后一修改。 [C11标准,第7.17.3节,第9段。]

  • 对于原子操作甲和乙上的原子对象中号,其中甲 修改中号和乙取它的值,如果有一个 memory_order_seq_cst围栏X,使得甲测序-前X 和乙如下X在小号,然后乙观察任一的影响的甲 或更高修改中号在其变形例的顺序。 [C11标准,第7.17.3节,第10段。]

  • 对于原子操作甲和乙上的原子对象中号,其中甲 修改中号和乙取其值,如果有 memory_order_seq_cst围栏X和ÿ使得甲测序-前X,ÿ测序-之前乙,和X之前ÿ 在S中,然后B观察到A的影响或M的后续修改(按其修改顺序)。 [C11标准,第7.17.3节,第11段。]

  • 对于原子操作甲和乙上的原子对象中号,如果有 memory_order_seq_cst围栏X和ÿ使得甲测序-前X,ÿ测序-之前乙,和X先ÿ 在小号,然后乙迟发生甲在M的修改顺序。

  • memory_order_seq_cst仅针对以下程序确保顺序一致性:(1)没有数据争用,并且(2)仅使用 memory_order_seq_cst同步操作。除非特别注意,否则使用任何较弱的订购都将使此保证无效。特别是,memory_order_seq_cst栅栏仅确保栅栏本身的总顺序。通常,栅栏不能用于恢复顺序规格较弱的原子操作的顺序一致性。

  • 原子读取-修改-写入操作应始终读取在与读取-修改-写入操作关联的写入之前存储的最后一个值(按修改顺序)。 [C11标准,第7.17.3节,第12段。]

  • 实现应确保不会循环地依赖于其自身的计算来计算“空中”值。

注意:根据上述规则,并且独立于先前的脚注C ++问题,在以下有问题的示例中,已知x == y == 42是有效的最终状态:

这不是有用的行为,实现不应利用此现象。可以预期,将来OpenCL委员会对内存模型描述进行适当的更新可能会不允许这样做。

实现应使原子存储在合理的时间内对原子负载可见。 [C11标准,第7.17.3节,第16段。]

只要满足以下条件,与在一个或多个OpenCL设备上执行的内核共享SVM内存的主机程序就可以使用原子操作和同步操作来确保彼此之间的分配以及与内核的分配是可见的:

  • 必须使用细粒度的缓冲区或细粒度的系统SVM来共享内存。尽管粗粒度缓冲区SVM分配可以支持原子操作,但是除了映射和取消映射操作外,不能保证这些分配的可见性。

  • CL​MEM​SVM_​ATOMICS设备必须支持通过提供标志指定的可选OpenCL 2.0 SVM原子控制的可见性,并且分配时应将标志提供给SVM缓冲区。

  • 主机原子操作和同步操作必须与OpenCL内核语言的兼容。这要求主机原子操作所作用的数据类型的大小和表示形式与OpenCL内核语言原子类型一致。

如果满足这些条件,则主机操作将在all_svm_devices范围内应用。

内存排序规则:围栏操作

本节描述OpenCL 2.0防护操作如何促进本地和全局发生之前的关系。

之前,我们介绍了称为栅栏的同步原语。栅栏可以利用获取的memory_order,释放的memory_order或同时使用这两者。具有获取语义的围墙称为获取围墙;具有发布语义的隔离网称为发布隔离网。“ 原子和篱笆操作 ” 概述部分描述了导致获取和释放篱笆的内存顺序。

如果存在原子操作X和Y都在某个全局原子对象M上进行操作,则全局释放围墙A与全局获取围墙B进行全局同步- 使得A在X之前进行排序,X 修改M,而Y在先进行排序-在B之前,Y读取X写入的值 或假设的释放序列X中任何副作用写入的值(如果是释放操作)将到达X,并且A,B的范围包括在内。 [C11标准,第7.17.4节,第2段,已修改。]

全局释放栅栏A与原子操作B全局同步,如果存在原子操作X使得A被排序,则该操作B 对全局原子对象M执行获取操作-在X之前,X 修改M,B读取写入的值由X或通过任何副作用在假想释放顺序写入的一个值X好像它是一个释放操作将朝向,和的范围甲和乙是包括端值。 [C11标准,第7.17.4节,第3段,已修改。]

如果在M上存在一些原子操作X使得X在B之前被排序 并且读取由A或B写入的值,则作为对全局原子对象M的释放操作的原子操作A与全局获取栅栏B全局同步。在A开头的释放顺序中由任何副作用写入的值,并且A和B的范围包括端值。 [C11标准,第7.17.4节,第4段,已修改。]

如果存在原子操作X和Y都在某个本地原子对象M上进行操作,则本地释放防护A与本地获取防护B进行本地同步,从而使A在X之前进行排序,X修改M, Y进行排序-在B之前,并且Y读取X写入的值或假设释放序列中任何副作用写入的值X 如果为a

释放操作,并且A和B的范围包括在内。 [C11标准,第7.17.4节,第2段,已修改。]

本地释放防护A与原子操作B 进行本地同步,如果存在原子操作X使得A在X之前进行排序,X 修改M,并且B读取值,则原子操作B与本地原子对象M进行获取操作如果是释放操作,则由X编写的X或由假设的释放序列X中的任何副作用写入的值都会发生,并且A和B的范围包括端值。 [C11标准,第7.17.4节,第3段,已修改。]

如果在M上存在一些原子操作X使得X在B之前被排序并且读取由A或B写入的值,则原子操作A是对本地原子对象M的释放操作, 与本地获取隔离区B本地同步。在A开头的释放顺序中由任何副作用写入的值,并且A和B的范围包括端值。 [C11标准,第7.17.4节,第4段,已修改。]

假定X和Y是两个工作项隔离物,每个隔离物都设置了CLK_GLOBAL_MEM_FENCE和CLK_LOCAL_MEM_FENCE标志。 X全球进行同步,与Ÿ和X当地下同步与Ÿ如果需要的条件X一起全球同步Ÿ得到满足,所需的条件X到本地同步,与Ÿ得到满足,或者满足的条件两套。

内存排序规则:工作组功能

OpenCL内核执行模型包括单个工作组中各个工作项的集体操作。这些称为工作组功能。除了工作组屏障功能外,它们还包括SPIR-V IL规范中描述的扫描,缩小和管道工作组功能。我们将首先讨论工作组的障碍。随后将讨论其他工作组功能。

屏障功能为内核提供了一种在单个工作组内同步工作项目的机制:非正式地,工作组的每个工作项目必须在执行任何工作之前执行屏障。它还以类似于篱笆的方式将存储操作排序为一个或多个地址空间的指定组合,例如本地存储器或全局存储器。

为了精确指定屏障的内存排序语义,我们需要区分对屏障调用的动态实例和静态实例。例如,对障碍的调用可以出现在循环中,并且每次执行相同的静态障碍调用都会导致障碍的新动态实例,该实例将独立同步工作组工作项。

执行障碍的动态实例的工作项会导致两个操作,即两个围栏,分别称为入口围栏和出口围栏。这些围栏遵循本章其他地方指定的所有围栏规则以及以下内容:

  • 入口围栏是释放围栏,其标志和作用域与屏障要求的相同。

  • 出口围栏是获取围栏,其标志和作用域与障碍要求的相同。

  • 对于每个工作项目,入口围栏在出口围栏之前排序。

  • 如果标记设置了CLK_GLOBAL_MEM_FENCE,则对于每个工作项,入口围栅与同一工作组中的所有其他工作项的出口围栅进行全局同步。

  • 如果标记设置了CLK_LOCAL_MEM_FENCE,则对于每个工作项目,入口围栏都会与同一工作组中所有其他工作项的出口围栏进行本地同步。

其他工作组函数包括诸如work_group_all()和work_group_broadcast()之类的函数,并以内核语言和IL规范进行了描述。这些工作组功能的使用意味着在单个工作项的执行内的语句之间的先于顺序关系,以满足数据依赖性。例如,为工作组功能提供值的工作项必须表现为好像在开始执行该工作组功能之前就已生成该值。此外,程序员必须确保工作组中的所有工作项必须执行相同的工作组功能调用站点或动态工作组功能实例。

内存排序规则:子组功能

OpenCL内核执行模型在单个子组中包括跨工作项的集体操作。这些称为子组功能。除了子组屏障功能外,它们还包括SPIR-V IL规范中描述的扫描,缩小和管道子组功能。我们将首先讨论分组障碍。其他小组功能将在后面讨论。

屏障功能为内核提供了一种在单个子组中同步工作项的机制:非正式地,子组中的每个工作项必须在执行任何屏障之前执行屏障。它还以类似于篱笆的方式将存储操作排序为一个或多个地址空间的指定组合,例如本地存储器或全局存储器。

为了精确指定屏障的内存排序语义,我们需要区分对屏障调用的动态实例和静态实例。例如,对障碍的调用可能出现在循环中,并且每次执行相同的静态障碍调用都会导致该障碍的新动态实例,该实例将独立同步子组工作项。

执行障碍的动态实例的工作项会导致两个操作,即两个围栏,分别称为入口围栏和出口围栏。这些围栏遵循本章其他地方指定的所有围栏规则以及以下内容:

  • 入口围栏是释放围栏,其标志和作用域与屏障要求的相同。

  • 出口围栏是获取围栏,其标志和作用域与障碍要求的相同。

  • 对于每个工作项目,入口围栏在出口围栏之前排序。

  • 如果标记设置了CLK_GLOBAL_MEM_FENCE,则对于每个工作项目,入口围栏与同一子组中的所有其他工作项目的出口围栏进行全局同步。

  • 如果标志设置了CLK_LOCAL_MEM_FENCE,则对于每个工作项目,入口围栏与同一子组中的所有其他工作项的出口围栏进行本地同步。

其他子组功能包括诸如sub_group_all()和sub_group_broadcast()之类的功能,这些功能在OpenCL内核语言规范中进行了描述。这些子组功能的使用意味着在单个工作项的执行内的语句之间的先于顺序关系,以满足数据依赖性。例如,为子组功能提供值的工作项必须表现为好像在开始执行该子组功能之前生成该值。此外,程序员必须确保子组中的所有工作项必须执行相同的子组功能调用站点或动态子组功能实例。

内存排序规则:主机端和设备端命令

本节描述与命令队列关联的OpenCL API函数如何促进事前发生关系。OpenCL 2.0中有两种类型的命令队列和关联的API函数:主机命令队列和设备命令队列。这些命令队列与内存模型的交互在很大程度上是等效的。在某些情况下,这些规则仅适用于主机命令队列。我们将通过在内存排序规则中专门表示主机命令队列来指示这些特殊情况。在这种情况下,SVM内存一致性仅暗示于同步主机命令。

本节中的内存排序规则适用于所有内存对象(缓冲区,图像和管道)以及不适用更早,更细粒度规则的SVM分配。

在本节的其余部分中,我们假定排队到命令队列上的每个命令C都有一个关联的事件对象E,该事件对象E发出信号通知其执行状态,而不管E是否返回到排队了C的执行单元。我们还区分API函数调用入列的命令之间 ç和创建一个事件Ë,执行Ç,和完成 Ç(其标记事件ë作为完成)。

API命令的排序和同步规则定义如下:

  • 如果API函数调用X使命令C排队,则X 与C全局同步。例如,用于使内核排队的主机API函数与该内核实例的执行开始进行全局同步,以便在排队内核函数调用之前将顺序全局执行的内存更新在任何内核读取或写入内核队列之前进行相同的内存位置。对于设备端队列,仅在细粒度SVM的情况下,全局内存更新才在X 发生之前顺序排序-在C读写这些内存位置之前。

  • 如果E是命令C等待的事件,则E 与C全局同步。特别是,如果Ç在等待事件Ë被跟踪命令的执行状态C1,然后进行存储操作 C1将全球发生,之前的内存操作所做Ç。例如,假设我们有一个使用粗粒度SVM共享的OpenCL程序,它将内核排入主机命令队列,以操纵缓冲区的内容,在内核完成后,主机线程可以访问该缓冲区的区域。为此,主机线程可以调用clEnqueueMapBuffer将阻塞模式的映射命令加入队列以映射该缓冲区,并指定该映射命令必须等待表示内核完成的事件。当clEnqueueMapBuffer返回时,内核对该缓冲区执行的任何内存操作都将全局发生-在主机线程进行后续内存操作之前。

  • 如果命令C具有事件E来表示其完成,则C 与E全局同步。

  • 对于排队在主机端命令队列中的命令C,如果C具有事件E来指示其完成,则E与等待E的API调用X全局同步。例如,如果主机线程或内核实例在E上调用了事件等待功能(例如,从主机线程调用的clWaitForEvents函数),则E与该事件等待功能进行全局同步。

  • 如果将命令C和C1按该顺序排队到有序命令队列中,则表明C的完成与C1全局同步的事件(包括由于有序队列而在C 和C1之间隐含的事件)。请注意,在OpenCL 2.0中,只能将主机命令队列配置为有序队列。

  • 如果API调用将带有空事件列表的标记命令C排入队列,C应当等待该事件的空列表,则所有命令的事件都在C之前在命令队列global-synchronize-with C中排队。

  • 如果主机API调用使命令队列屏障命令C带有空的事件列表(C应当等待该事件列表)进入队列,那么所有命令的事件都在C之前在命令队列global-synchronize-with C中排队。另外,表示C的完成的事件 全局同步-在命令队列中的C之后排入所有命令。

  • 如果主机线程执行clFinish调用X,则在命令队列global-synchronizes-with X中,所有在X之前排队的命令的事件。

  • 内核实例的开始ķ全球进行同步,与工作项目的所有操作ķ。请注意,这包括使用细粒度SVM在程序中通过工作项执行任何原子操作。

  • 内核实例K的所有工作项的所有操作都是 全局同步的,该事件表示K的完成。请注意,这还包括使用细粒度SVM在程序中通过工作项执行任何原子操作。

  • 如果在事件E上注册了回调过程P,则E 与P的所有操作全局同步。请注意,仅针对主机命令队列中的命令定义了回调过程。

  • 如果C是等待事件E完成的命令,并且API函数调用X将用户事件E的状态设置为 CL_​COMPLETE(例如,从使用clSetUserEventStatus函数的主机线程 ),则X全局-与C同步。

  • 如果设备使用CLK_ENQUEUE_FLAGS_WAIT_KERNEL标志使命令C排队,则父内核实例的结束状态将与C全局同步。

  • 如果工作组使用CLK_ENQUEUE_FLAGS_WAIT_WORK_GROUP标志使命令C排队,则工作组的结束状态将与C全局同步。

当使用乱序命令队列时,可以使用事件等待或标记或命令队列屏障命令来确保相关命令的正确排序。在这些情况下,等待事件或标记或屏障命令将提供必要的全局与关系。

在这个情况下:

  • 当使用从主机排队的来自不同内核实例的原子操作时,访问单个cl_mem对象中的共享位置或不相交的位置,使得一个或多个原子操作为写操作是实现定义的,并且除同步点外,不能保证正确的行为。

  • 根据本节前面介绍的内存排序规则,可以保证在使用来自不同内核实例的原子操作时,可以访问单个cl_mem对象中的共享位置或不相交的位置,这些内核实例包括父内核和该内核排队的任何数量的子内核。

  • 根据所描述的内存排序规则,可以保证在使用从主机排队到不同设备的不同内核实例的原子操作时,可以访问单个程序范围全局变量,粗粒度SVM分配或细粒度SVM分配中的共享位置或不相交位置本节前面的内容。

如果使用细粒度的SVM但不支持OpenCL 2.0原子操作,则主机和设备可以同时读取相同的内存位置,并且可以同时更新非重叠的内存区域,但是尝试更新相同的内存位置是不确定的。在OpenCL同步点可以确保内存一致性,而无需调用clEnqueueMapBuffer和 clEnqueueUnmapMemObject。对于细粒度的SVM缓冲区,可以确保在同步点仅更新内核写入的值。不能引入对原始程序中未包含的细粒度SVM缓冲区的写操作。

在本节的其余部分,我们将讨论有关带有主机命令队列的命令的排序规则的几点。

OpenCL 1.2标准将同步点描述为内核实例或主机程序位置,在此位置,不同的工作项或命令队列命令可见的内存内容相同。它还说,等待事件和命令队列障碍是命令队列中命令之间的同步点。上面列出的四个规则(2、4、7和8)涵盖了这些OpenCL同步点。

允许在非SVM缓冲区或粗粒度SVM缓冲区上执行的映射操作(clEnqueueMapBuffer或clEnqueueMapImage)使用最新的数据运行时视图覆盖整个目标区域,如通过映射操作同步的命令所看到的那样,值是否由执行内核写入。当与映射操作同步的内核正在执行时,由另一个内核或主机线程在此区域内更改的任何值都可能被映射操作覆盖。

在主机命令之间的同步点处对非SVM cl_mem缓冲区和粗粒度SVM分配的访问进行了排序。在存在乱序命令队列或映射到同一设备的一组命令队列的情况下,多个内核实例可以在同一设备上同时执行。

Last updated

Was this helpful?