为什么__sync_add_and_fetch在32位系统上为64位变量工作?[英] Why does __sync_add_and_fetch work for a 64 bit variable on a 32 bit system?

本文是小编为大家收集整理的关于为什么__sync_add_and_fetch在32位系统上为64位变量工作?的处理方法,想解了为什么__sync_add_and_fetch在32位系统上为64位变量工作?的问题怎么解决?为什么__sync_add_and_fetch在32位系统上为64位变量工作?问题的解决办法?那么可以参考本文帮助大家快速定位并解决问题。

问题描述

考虑以下浓缩代码:

/* Compile: gcc -pthread -m32 -ansi x.c */
#include <stdio.h>
#include <inttypes.h>
#include <pthread.h>

static volatile uint64_t v = 0;

void *func (void *x) {
    __sync_add_and_fetch (&v, 1);
    return x;
}

int main (void) {
    pthread_t t;
    pthread_create (&t, NULL, func, NULL);
    pthread_join (t, NULL);
    printf ("v = %"PRIu64"\n", v);
    return 0;
}

我有一个我想要递增的uint64_t变量原子,因为变量是多线程程序中的计数器. 为了实现原子性,我使用gcc的原子内置.

如果我编译AMD64系统(-m64),所生产的汇编器代码易于理解. 通过使用lock addq,处理器保证增量是原子的.

 400660:       f0 48 83 05 d7 09 20    lock addq $0x1,0x2009d7(%rip)

但相同的C代码在IA32系统上产生非常复杂的ASM代码(-m32):

804855a:       a1 28 a0 04 08          mov    0x804a028,%eax
804855f:       8b 15 2c a0 04 08       mov    0x804a02c,%edx
8048565:       89 c1                   mov    %eax,%ecx
8048567:       89 d3                   mov    %edx,%ebx
8048569:       83 c1 01                add    $0x1,%ecx
804856c:       83 d3 00                adc    $0x0,%ebx
804856f:       89 ce                   mov    %ecx,%esi
8048571:       89 d9                   mov    %ebx,%ecx
8048573:       89 f3                   mov    %esi,%ebx
8048575:       f0 0f c7 0d 28 a0 04    lock cmpxchg8b 0x804a028
804857c:       08 
804857d:       75 e6                   jne    8048565 <func+0x15>

这是我不明白的:

如果有一个"脏读",那么它可能没关系,但可以有人请概述一个短的<强>证明没有问题?

更进一步:为什么生成的代码跳回0x8048565而不是0x804855a?我是积极的,这只是其他作家的唯一纠正,只有递增变量.这是对__sync_add_and_fetch功能的牵连要求吗?

推荐答案

初始用2个单独的mov指令读取不是原子,但它不在循环中. @ Interjay的答案解释为什么这很好.


乐趣事实:即使没有lock前缀,cmpxchg8b的读数也会是原子的. (但是这个代码做了前缀,使整个RMW操作原子,而不是单独的原子负载和原子存储.)

它保证由于它正确对齐(并且它适合一个缓存线),并且由于英特尔通过这种方式进行了规格,请参阅英特尔架构手册第1卷,4.4.1:

交叉4字节边界的单词或双字操作数 考虑交叉8字节边界的Quadword操作数 未对齐,需要两个单独的内存总线循环进行访问.

Vol 3a 8.1.1:

Pentium处理器(自更新的处理器以来)保证了 始终执行以下内存操作之后 原子上:

•在64位上读取或写入Quadword 边界

•16位访问适合的未生成的内存位置 在32位数据总线中

p6家庭处理器(和较新的 处理器以来)保证以下附加内存 操作将始终以原子载方式进行:

•未对准16-,32-, 和64位访问缓存内存适合缓存行

因此,通过对齐,可以在1个循环中读取它,并且它适合一个缓存行,使cmpxchg8b读取原子.

如果数据未对准,则lock前缀将静止使其成为原子,但性能成本将是非常高,因为简单的缓存锁定(延迟对MESI无效的响应无效的一个缓存行的请求)将不再足够.


代码跳回0x8048565(在mov加载之后,包括副本和添加-1),因为v已经加载;如果CMPXCHG8B将EAX:EDX将EAX:EDX设置为目标,则无需将其设置为

CMPXCHG8B英特尔ISA手动Vol的描述. 2a:

比较EDX:带M64的EAX.如果是相等的,请将ZF和加载ECX:EBX进入M64. 否则,清除ZF和将M64加载到EDX:EAX.

因此代码仅需要递增新返回的值,然后重试. 如果我们在C代码中查看这一点,它变得更轻松:

value = dest;                    // non-atomic but usually won't tear
while(!CAS8B(&dest,value,value + 1))
{
    value = dest;                // atomic; part of lock cmpxchg8b
}

value = dest实际上是与比较部分使用的cmpxchg8b相同的读取.循环内没有单独的重新加载.

实际上, c11 atomic_compare_exchange_weak/_strong 有这个行为内置:它更新"预期"操作数.

所以gcc的现代内置 __atomic_compare_exchange_n (type *ptr, type *expected, type desired, bool weak, int success_memorder, int failure_memorder) - 它需要通过引用的值expected值.

与gcc的旧的过时__sync内置,__sync_val_compare_and_swap返回旧的val(而不是boolean交换/not-swap结果__sync_bool_compare_and_swap)

其他推荐答案

0x804855a和0x804855f中的变量读数不需要是原子的.使用比较和交换指令在伪代码中的增量如此如此:

oldValue = *dest; // non-atomic: tearing between the halves is unlikely but possible
do {
    newValue = oldValue+1;
} while (!compare_and_swap(dest, &oldValue, newValue));

由于比较 - 和交换检查在交换之前检查*dest == oldValue,它将充当保障 - 以便如果oldValue中的值不正确,则循环将再次尝试,因此如果没有问题非原子读取的值不正确.

通过lock cmpxchg8b 完成的64位访问*dest 原子(作为*dest的原子RMW的一部分).在这里分别加载2个半部的任何撕裂都会被抓住.或者,如果在初始读取后发生另一个核心,则在lock cmpxchg8b:即使使用单址宽度cmpxchg -Retry循环也是可能的. (例如,实现原子捕获_MUL或原子学float,或X86的lock前缀的其他RMW操作不会让我们直接执行.)


你的第二个问题是为什么oldValue = *dest不在循环中.这是因为compare_and_swap函数将始终替换为oldValue的值*dest.所以它基本上是为你的oldValue = *dest执行线路,并且没有点钟再做.在cmpxchg8b指令的情况下,当比较失败时,它将将内存操作数的内容放在edx:eax中.

compare_and_swap的pseudocode是:

bool compare_and_swap (int *dest, int *oldVal, int newVal)
{
  do atomically {
    if ( *oldVal == *dest ) {
        *dest = newVal;
        return true;
    } else {
        *oldVal = *dest;
        return false;
    }
  }
}
顺便说一下,在您的代码中,您需要确保v对齐至64位 - 否则可以在两个高速缓存行之间分开,而cmpxchg8b指令不会原子地执行.您可以使用GCC的__attribute__((aligned(8))).

本文地址:https://www.itbaoku.cn/post/359205.html