如何在C ++中以原子方式添加和获取128位数字?

6

我使用的是Linux x86_64和clang 3.3。

从理论上讲,这可行吗?

std::atomic<__int128_t>不起作用(对某些函数的引用未定义)。

__atomic_add_fetch也不起作用('error: cannot compile this atomic library call yet')。

std::atomic__atomic_add_fetch都适用于64位数字。

3个回答

10

无法使用单个指令完成此操作,但是您可以模拟它并且仍然是无锁的。除了最早的AMD64处理器之外,x64支持 CMPXCHG16B 指令。通过一些多精度数学运算,您可以很容易地完成此操作。

我不知道GCC中 CMPXCHG16B 的内置函数,但希望您能理解使用 CMPXCHG16B 自旋循环的想法。下面是VC++的一些未测试代码:

// atomically adds 128-bit src to dst, with src getting the old dst.
void fetch_add_128b(uint64_t *dst, uint64_t* src)
{
    uint64_t srclo, srchi, olddst[2], exchlo, exchhi;

    srchi = src[0];
    srclo = src[1];
    olddst[0] = dst[0];
    olddst[1] = dst[1];

    do
    {
        exchlo = srclo + olddst[1];
        exchhi = srchi + olddst[0] + (exchlo < srclo); // add and carry
    }
    while(!_InterlockedCompareExchange128((long long*)dst,
                                          exchhi, exchlo,
                                          (long long*)olddst));

    src[0] = olddst[0];
    src[1] = olddst[1];
}

编辑:以下是一些未经测试的代码,基于我所找到的GCC内置函数:

// atomically adds 128-bit src to dst, returning the old dst.
__uint128_t fetch_add_128b(__uint128_t *dst, __uint128_t src)
{
    __uint128_t dstval, olddst;

    dstval = *dst;

    do
    {
        olddst = dstval;
        dstval = __sync_val_compare_and_swap(dst, dstval, dstval + src);
    }
    while(dstval != olddst);

    return dstval;
}

你为什么要发那个帖子,让我的内联汇编看起来这么糟糕呢 ;) - Mats Petersson
哈!好吧,VC++优化那段代码就像垃圾一样,而且不支持64位的内联汇编,所以不要感到难过。 - Cory Nelson

3

是的,您需要告诉编译器您正在支持它的硬件。

本答案将假定您正在使用x86-64;arm可能有类似的规格。

从通用的x86-64 微架构级别来看,您至少需要 x86-64-v2 以让编译器知道您具有 cmpxchg16b 指令。

这里有一个可工作的godbolt,请注意编译器标志 -march=x86-64-v2https://godbolt.org/z/PvaojqGcx

有关x86-64-psABI的更多阅读材料,规范发布在此处


2

这是不可能的。没有一条x86-64指令可以在一条指令中完成128位加法,要想做到原子性操作,基本的起点是它必须是单个指令(即使有一些指令在这种情况下也不是原子性的,但那是另一回事)。

你需要在128位数字周围使用其他锁定机制。

编辑:有可能会想出一些使用类似以下内容的东西:

 __volatile__ __asm__(
    "     mov            %0, %%rax\n"
    "     mov            %0+4, %%rdx\n"
    "     mov            %1,%%rbx\n"
    "     mov            %1+4,%%rcx\n"
    "1:\n
    "     add            %%rax, %%rbx\n"
    "     adc            %%rdx, %%rcx\n"
    "     lock;cmpxcchg16b %0\n"
    "     jnz            1b\n"
    : "=0"
    : "0"(&arg1), "1"(&arg2));

这只是我随手编写的一些代码,并没有进行编译,更不用说验证它是否有效了。但原理是它会一直重复,直到比较相等为止。

编辑2:打字太慢了,Cory Nelson刚刚发布了相同的东西,但使用了内置函数。

编辑3:更新循环以避免读取不需要读取的内存... CMPXCHG16B将为我们完成这项工作。


您的说法“要以原子方式执行某些操作,基本起点是它是单个指令”是不准确的。它要么(1)必须是单个指令,或者(2)处理器需要支持诸如LL/SC、CMPXCHG或硬件事务性内存之类的操作,允许使用这些操作的无锁自旋循环来创建更复杂的原子操作。或者(3)您需要使用锁定协议。 - Krazy Glew
@MatsPetersson,你需要在cmpxcchg16b前面加上一个lock吗? - ergohack
@ergohack:我认为你是对的,我已经添加了一个锁。 - Mats Petersson

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