ARM64: LDXR/STXR与LDAXR/STLXR的区别

13
在iOS上,有两个类似的函数OSAtomicAdd32OSAtomicAdd32Barrier。我想知道你什么时候需要使用Barrier变体。
反汇编后,它们是:
_OSAtomicAdd32:
ldxr    w8, [x1]
add     w8, w8, w0
stxr    w9, w8, [x1]
cbnz    w9, _OSAtomicAdd32
mov     x0, x8
ret     lr

_OSAtomicAdd32Barrier:
ldaxr   w8, [x1]
add     w8, w8, w0
stlxr   w9, w8, [x1]
cbnz    w9, _OSAtomicAdd32Barrier
mov     x0, x8
ret     lr
在哪些场景下需要后者的Load-Acquire / Store-Release语义?LDXR/STXR指令可以被重新排序吗?如果可以,那么在没有屏障的情况下,原子更新是否可能会“丢失”?根据我所读的,似乎不会发生这种情况,如果是这样,那么为什么需要Barrier变体?也许只有在您还需要DMB进行其他用途时才需要它?
谢谢!
3个回答

20

哦,弱内存排序的可怕之处...

第一个片段是基本的原子读-修改-写操作 - 如果其他人触及了x1指向的任何地址,存储排除将失败,并且会一直尝试直到成功。到目前为止还不错。然而,这仅适用于独占监视器所涵盖的地址(或更正确地说是区域),因此虽然它对于原子性很好,但对于除该值以外的任何同步都是无效的。

考虑这样一种情况,其中CPU1正在等待CPU0将一些数据写入缓冲区。CPU1坐在那里等待某种同步对象(假设是信号量),等待CPU0更新它以发出新数据已准备好的信号。

  1. CPU0写入数据地址。
  2. CPU0增加信号量(原子地进行),该信号量恰好在内存中的其他位置。
  3. ???
  4. CPU1看到新的信号量值。
  5. CPU1读取一些数据,这可能是旧数据、新数据或两者混合。

现在,第三步发生了什么?可能所有事情都是按顺序发生的。很可能,硬件决定,由于没有地址依赖性,它将允许信号量存储器先于数据地址存储器进行存储。也许信号量存储器在缓存中被命中,而数据没有。也许只是因为那些硬件人员才能理解的复杂原因而这样做。无论哪种方式,CPU1完全有可能在新数据到达内存之前看到信号量更新,从而读回无效数据。

要解决这个问题,CPU0必须在步骤1和2之间设置障碍,以确保数据已经被写入,然后再写入信号量。使原子写成为障碍是一个不错的简单方法。但是,由于障碍会严重降低性能,因此您还需要轻量级的无障碍版本,以适用于不需要这种完全同步的情况。

现在,更加令人费解的部分是,CPU1也可以重新排序其加载。同样,由于没有地址依赖性,它可以自由地推测在CPU0的障碍之前加载数据,而不考虑信号量的加载。因此,在步骤4和5之间,CPU1还需要自己的障碍。

如果您想了解更权威的内容,可以阅读ARM的屏障测试和手册。请注意,这些内容可能会让人感到困惑。

另外,就本例而言,获取/释放的体系结构语义进一步复杂化了事情。由于它们只是单向屏障,因此虽然OSAtomicAdd32Barrier相对于其前后代码添加了完整的屏障,但它实际上并不保证与原子操作本身的顺序有关-请参见来自Linux的此讨论以获得更多解释。当然,这是从架构的理论角度来看的;实际上,A7硬件很可能采用“简单”选项,将LDAXR连接起来,只需执行DMB+LDXR等操作,这意味着他们可以摆脱这种情况,因为他们可以根据自己的实现进行编码,而不是规范。


感谢您写得详细清晰的回答!我希望我能接受多个答案。在发布问题之前,我确实阅读了一些屏障试验的内容,但这些答案确实帮助我澄清了困惑。 - Dave Lee
@DaveLee,说实话,这回答了一个问题:“在没有障碍的情况下,原子更新是否可能会‘丢失’?”而“被接受”的答案并没有。事实上,最好不要接受答案,这样我们就可以按投票结果排序回答。Notlikethat的答案对更多人来说确实更有帮助; SO将其隐藏起来让某些随机用户获得10个额外的“点数”或其他他们现在分发的东西,这真的毫无意义。 - SO_fix_the_vote_sorting_bug

8

OSAtomicAdd32Barrier() 存在的目的是为那些不仅仅是原子增量而还实现自己的基于OSAtomicAdd()多处理同步原语的人提供帮助。例如,创建自己的互斥库。OSAtomicAdd32Barrier()使用重型屏障指令来强制执行原子操作两侧的内存排序。这在正常使用中是不可取的。

总结:

1)如果您只想以线程安全的方式递增整数,请使用OSAtomicAdd32()

2)如果您被一堆愚蠢地假设OSAtomicAdd32()可以用作跨处理器内存排序和推测障碍的旧代码所困扰,请将其替换为OSAtomicAdd32Barrier()


1
谢谢!并将您总结的第一点推广到其他平台:“如果您只想以线程安全的方式增加整数,请使用ldxraddstxr在循环中直到stxr成功为止”。 - ahcox

3
我猜这只是一种复制现有的与架构无关的操作语义的方式。
使用ldaxr/stlxr对,上述序列将确保正确的排序,如果AtomicAdd32用作同步机制(互斥锁/信号量)-无论结果的高级操作是获取还是释放。
因此,这不是为了强制执行原子添加的一致性,而是为了强制执行在获取/释放互斥锁和对受该互斥锁保护的资源执行的任何操作之间的顺序。
它比您在正常本地同步机制中使用的ldxar/stxrldxr/stlxr效率低,但如果您有现有的平台无关代码期望具有这些语义的原子添加,则这可能是实现它的最佳方法。

谢谢!说它是为了“再现现有的独立于架构的语义”,这是一个非常有帮助的角度去理解。非常感谢。 - Dave Lee

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接