LLVM IR 内存访问

易失性(volatile)内存访问

某些内存访问,如load‘s,store‘s和llvm.memcpy‘s可能被标记volatile。优化器不得更改易失性操作的数量或更改其相对于其他volatile操作的执行顺序。优化器可以改变相对于非volatile操作的易失性操作的顺序。这不是Java的“volatile”,并且没有跨线程同步行为。

易失性加载或存储可能具有其他特定于目标的语义。
任何易失性操作都可能产生副作用,并且任何易失性操作都可以读取和/或修改无法通过此模块中的常规加载或存储访问的状态。
易失性操作可能使用不指向存储器的地址(如MMIO寄存器)。
这意味着编译器可能不会使用volatile操作来证明对该地址的非易失性访问具有已定义的行为。

挥发性访问的允许副作用是有限的。
如果给定地址的非易失性存储是合法的,则易失性操作可以修改该地址处的存储器。
易失性操作可能不会修改正在编译的模块可访问的任何其他内存。
易失性操作可能无法调用当前模块中的任何代码。

编译器可以假设在易失性操作之后将继续执行,因此可以在易失性操作之后提升修改存储器或可能具有未定义行为的操作。

即使那些内在函数被标记为volatile,IR级别的易失性加载和存储也无法安全地优化为llvm.memcpy或llvm.memmove内部函数。
同样,后端不应该拆分或合并目标合法的易失性加载/存储指令。

这是合理的:

平台可能依赖volatile加载,并且本地支持的数据宽度存储将作为单条指令执行。例如,在C中,这适用于具有本地硬件支持的易失性基本类型的l值,但不一定适用于聚合类型。前端支持这些预期,这在IR中是故意没有说明的。上述规则确保IR转换不会违反前端(应该是前置的语言)与该语言的合同。

并发操作的内存模型

LLVM IR没有定义任何启动并行执行线程或注册信号处理程序的方法。尽管如此,还是有特定于平台的方式来创建它们,并且我们定义LLVM
IR他们存在的行为。该模型受C++ 0x内存模型的启发。

有关此模型的更多非正式介绍,请参阅LLVM原子指令和并发指南

我们将发生之前的偏序定义为最小偏序(partial order).

  • 是单线程程序顺序的超集,并且
  • 当同步b时,包含一个从ab的边缘。通过特定于平台的技术(如pthread锁,线程创建,线程连接等)以及原子指令引入同步对。(另请参阅原子内存排序约束)。

请注意,程序顺序不会在线程和该线程内执行的信号之间引入边界之前发生的事件。

每个(定义的)读取操作(加载指令,memcpy,原子加载/读取-修改-写入等)R读取由(定义的)写入操作写入的一系列字节(存储指令,原子存储/读取-修改-写入,memcpy等)。就本节而言,已初始化的全局变量被认为是写入了初始化程序,它是原子化的,并且在任何其他读或写有问题的内存之前发生。对于读R的每个字节,Rbyte 可能会看到对相同字节的任何写入,除了:

  • 如果write1 发生在write2之前,并且write2发生在Rbyte之前,则Rbyte不会看到Write1
  • 如果Rbyte在write3之前发生,则Rbyte不会看到write3

鉴于该定义,Rbyte定义如下:

  • 如果R是volatile的,则结果与目标相关。(Volatile应该提供可以在C/C++中支持sig_atomic_t的保证,并且可以用于访问不像正常内存那样行为的地址,它通常不会提供跨线程同步。)
  • 否则,如果没有写入Rbyte发生之前的相同字节,则Rbyte会对该字节返回undef
  • 否则,如果Rbyte可能只看到一次写入,则Rbyte将返回该写入写入的值。
  • 否则,如果R是原子的,并且所有写入的Rbyte可能看到的都是原子的,它将选择其中一个写入的值。请参阅原子内存排序约束部分了解如何进行选择的其他限制条件。
  • 否则Rbyte返回undef。

R返回由它读取的一系列字节组成的值。这意味着该值内的一些字节可能是undef,没有整个值的undef。请注意,这只定义了操作的语义;
这并不意味着目标将发出多个指令来读取一系列字节。

请注意,在没有使用任何原子内在函数的情况下,此模型仅对单线程执行所需的IR转换放置一个限制:将store引入可能不会被存储的字节一般是不允许的。(具体来说,在另一个线程可以写入和读取地址的情况下,引入一个store可以改变一个load,可以看到只有一个写入可能看到多个写入的load。)

原子内存排序约束

原子指令(cmpxchgatomicrmwfenceatomic
load
atomic
store
)使用排序参数来确定与它们同步的同一地址上的其他原子指令。这些语义是从Java和C
++
0x中借用的,但是更通俗一点。如果这些描述不够精确,请检查这些规格(请参阅Atomic指南中的规格参考)。fence指令对待这些排序有些不同,因为他们没有收到地址。有关详细信息,请参阅该说明文档。

有关排序约束的更简单介绍,请参阅LLVM原子指令和并发指南

  • unordered:
    可以读取的一组值由发生前的部分顺序决定。除非某些操作写入,否则无法读取值。这旨在提供足够强大的保证来模拟Java的非volatile共享变量。此顺序不能指定为读取-修改-写入操作;
    它不足以使它们以任何有趣的方式成为原子。
  • monotonic:
    除了保证unordered之外,每个地址上的monotonic操作都有单个总顺序。所有修改顺序都必须与先发生的订单兼容。不能保证修改顺序可以合并到整个程序的全局总顺序中(而这通常是不可能的)。原子读取-修改-写入操作(cmpxchgatomicrmw)中的读取会在写入值之前立即读取修改顺序中的值。如果在同一地址的另一个原子读取之前发生一次原子读取,则稍后的读取必须在地址的修改顺序中看到相同的值或更高的值。这不允许重新排序monotonic(或更强大)的操作。如果地址是由一个线程monotonic-ally写入和其他线程monotonic-ally读取的
    – 反复读取该地址,其他线程最终必须看到写入。这对应于C++
    0x/C1x的memory_order_relaxed
  • acquire:
    除了monotonic的保障,一个进行同步边缘可以与形成release操作。这是为了模拟C++的memory_order_acquire
  • release:
    除了保证monotonic,如果此操作写入随后由acquire操作读取的值,则与该操作同步。(这不是一个完整的描述;请参阅发布序列的C++0x定义。)这对应于C
    ++ 0x/C1x的memory_order_release
  • acq_rel (获取+释放): acquirerelease操作作为地址的一部分。这对应于C
    ++ 0x / C1x memory_order_acq_rel
  • seq_cst (顺序一致):
    除了保证acq_relacquire对于仅读取的操作,release对于仅写入的操作),对于所有地址上的所有顺序一致的操作,存在全局总顺序,这与在部分顺序之前发生的以及与所有受影响地址的修改顺序一致。每个按顺序一致的读取将按照此全局顺序查看最后一个先前写入相同地址的内容。这对应于C
    ++ 0x / C1x memory_order_seq_cst和Java volatile

如果标记了一个原子操作syncscope("singlethread"),它只会同步并仅参与在同一线程中运行的其他操作(例如,在信号处理程序中)的seq_cst总排序。

如果标记了一个原子操作syncscope("<target-scope>"),其中 <target-scope>是目标特定的同步范围,那么它与目标相关,如果它与其他操作的seq_cst总排序同步并参与其中。

否则,未标记的原子操作syncscope("singlethread")syncscope("<target-scope>") 与同步和参与未标记syncscope("singlethread")syncscope("<target-scope>")的其它操作的seq_cst总排序