AArch64程序集中的原子链表LIFO,使用ldxr/stxr之间的加载或存储

AArch64程序集中的原子链表LIFO,使用ldxr/stxr之间的加载或存储,c,inline-assembly,arm64,lock-free,load-link-store-conditional,C,Inline Assembly,Arm64,Lock Free,Load Link Store Conditional,我使用ARMv8 64位汇编实现了共享内存上下文的后进先出 后进先出在开头插入一个节点,每个节点结构的第一个属性必须是下一个指针 这是实现后进先出原子插入和删除的正确程序集吗 它使用LDXR和STXR之间的额外加载或存储来读取head->next或将指针存储到新节点中 typedef union { void * head[1]; }lifo; int atomic_lifo_init(lifo * h) { if (h) { h->head[0]=NULL; } } in

我使用ARMv8 64位汇编实现了共享内存上下文的后进先出

后进先出在开头插入一个节点,每个节点结构的第一个属性必须是下一个指针

这是实现后进先出原子插入和删除的正确程序集吗

它使用LDXR和STXR之间的额外加载或存储来读取head->next或将指针存储到新节点中

typedef union {
   void * head[1];
}lifo;
int atomic_lifo_init(lifo * h) {
if (h) {
   h->head[0]=NULL;
  }
}
inline void *
 atomic_lifo_delete (lifo *h)
 {
         void    *ret = NULL;
         /*sa_ignore UNUSED_VAR*/
         void *  tmp = NULL;
 
         asm volatile ("\n"
                 "2:  ldxr      %0,[%2] \n" //load the head from h, which points the 1st node of list to local ret pointer.
                 "    cbz     %0, 3f \n" //check if the lifo is empty.
                 "    ldr      %1,[%0] \n" //store in tmp the 2nd node by derefencing the ret (h->head points 1st node. value of each node is next node as 1st attribute of node structure is pointing next.)
                 "    stxr     %w1, %1,[%2] \n" //update h->head with tmp.
                 "    cbnz     %w1, 2b \n" //continue if failed
                 "3:              \n"
                 : "=&r" (ret), "=&r" (tmp)
                 : "r" (h)
                 : "memory"
                 );
         return(ret);
 }
 
 /*
  * atomic_lifo_insert()
  *      Put an element on a list, protected against SMP
  */
 void
 atomic_lifo_insert (lifo *h, void *__new)
 {
         /*sa_ignore UNUSED_VAR*/
         void * next = NULL;
         void * flag = NULL;
         asm volatile (" \n"
                 "1: ldxr      %1,[%2] \n" //load head[0] from h,which points 1st node to local next pointer
                 "   str      %1,[%3] \n" //store the local next pointer to value of __new, as 1st attribute of the any node is next (convention used here). so __new's next is pointing current 1st node.
                 "   stxr    %w0, %3,[%2] \n" //update the h->head with 
   __next.
                 "   cbnz    %w0, 1b \n" //if stxr is failure try again.
                 : "=&r" (flag), "=&r" (next)
                 : "r" (h), "r" (__new)
                 : "memory"
                 );
 }

我对ARM assembly非常陌生,因此非常感谢您的帮助。

您的内联asm约束看起来是正确的,这应该按照您预期的方式编译。您可能可以使用
“+m”(*h)
让编译器选择寻址模式,而不是使用
“r”(h)
[%2]
对其进行硬编码

就排序而言,
ldr
是在ldxr之后排序的依赖项(如C11
内存\u顺序\u消耗
),因此可以工作

但是在
str
将地址发布到其他线程之后,
insert
中LL/SC之间的
str
才可能可见。因此我认为您需要
insert
中的
stlxr
(发布存储区)


在LDXR/STXR之间进行额外加载或存储不安全。Wikipedia提到,如果在LL和SC之间进行加载或存储,某些实现可能会错误地失败。Wiki说PowerPC确实允许这样做。但是AArch64通常不明确:根据ARM ref手册():

LoadExcl/StoreExcl循环只有在loadexclusive和storeexclusive之间没有显式内存访问时,才能保证向前进行

可能有一些AArch64 CPU会在其中创建无限循环的
stxr
故障,但也可能有其他CPU会在其中工作。如果它在您测试的CPU上实际工作,那么最好看看是否有任何文档支持它

如果
节点恰好与头节点位于同一缓存线(或LL/SC独占块)中,则这很可能是一个问题。如果这在你所关心的微体系结构上起作用,请确保你测试了这种情况,或者以某种方式使其不可能


除此之外,我认为你的整体算法看起来是正确的,所以如果你已经测试过它,并且发现它是有效的,那么这可能是好的


然而,我还没有仔细考虑过你的算法;我也没有任何设计或使用原始LL/SC的经验。我原则上知道它们是如何工作的,但我不准备说这绝对正确我从我所做的一点点思考中看不到任何问题,但这并不意味着没有任何问题。

您能用代码所做的来评论代码吗?我看到您在LL/SC事务中进行加载。这是用标准经济学无法表达的。但是,在不强制
stxr
失败的情况下,这真的有效吗?在
ldxr
之后对依赖项进行排序应该没问题,这是纯C11无法做到的另一件事。但是
insert
中事务内部的
str
可能会重新排序;您需要
stlxr
吗?另外,如果在
stxr
之前进行其他存储,我会更担心SC失败。我已经用注释更新了代码@PeterCordes@PeterCordes在insert str中,正在更新本地指针\uuu new(新节点),但stxr正在更新全局h。(其中h->head指向后进先出的第一个节点。)在正常测试中,它工作正常,但在共享内存上下文中,有时它失败。像某些进程一样,发现列表已变为空,但并非所有节点都被删除。同样的过程也适用于intel atomic lifo实现,其中使用了锁cmpxchange。感谢您的回复。因此,我也应该将ldxr更改为ldaxr。或者只有ldxr/then str然后stlxr。@souradeep:我认为您不需要为插入中的RMW订购
acq\u rel
,只要
release
。所以
ldxr/str/stlxr
。加上
str
本身在
ldxr
之后是依赖项顺序;存储数据来自加载,因此在加载从缓存读取之前,其他线程无法看到存储。谢谢,将从stxr修改为stlxr。我猜这个实现对于共享内存上下文也是安全的?意味着如果使用MAMP()在共享内存上创建LIFO头,并且多个进程可以插入删除节点。LL/SC循环中间的LDR不是一个好主意。从我的Arm体系结构参考手册(Arm DDI 0487C.a B2.9.5)副本中可以看出:“只有在负载独占和存储独占之间[…]没有显式内存访问时,才能保证LoadExcl/StoreExcl循环向前推进。”@JamesGreenhalgh:感谢文档中的引用,修改了我的答案。