带有GCC 7.3的__atomic_fetch_or的意外x64程序集

时间:2018-06-25 09:47:06

标签: c++ gcc assembly x86-64 compiler-bug

我试图使用64位积分作为位图,并自动获取/释放单个位的所有权。

为此,我编写了以下无锁代码:

#include <cstdint>
#include <atomic>

static constexpr std::uint64_t NO_INDEX = ~std::uint64_t(0);

class AtomicBitMap {
public:
    static constexpr std::uint64_t occupied() noexcept {
        return ~std::uint64_t(0);
    }

    std::uint64_t acquire() noexcept {
        while (true) {
            auto map = mData.load(std::memory_order_relaxed);
            if (map == occupied()) {
                return NO_INDEX;
            }

            std::uint64_t index = __builtin_ctzl(~map);
            auto previous =
                mData.fetch_or(bit(index), std::memory_order_relaxed);
            if ((previous & bit(index)) == 0) {
                return index;
            }
        }
    }

private:
    static constexpr std::uint64_t bit(std::uint64_t index) noexcept {
        return std::uint64_t(1) << index;
    }

    std::atomic_uint64_t mData{ 0 };
};

int main() {
    AtomicBitMap map;
    return map.acquire();
}

其中on godbolt,单独产生以下程序集:

main:
  mov QWORD PTR [rsp-8], 0
  jmp .L3
.L10:
  not rax
  rep bsf rax, rax
  mov edx, eax
  mov eax, eax
  lock bts QWORD PTR [rsp-8], rax
  jnc .L9
.L3:
  mov rax, QWORD PTR [rsp-8]
  cmp rax, -1
  jne .L10
  ret
.L9:
  movsx rax, edx
  ret

正是我所期望的 1

@Jester英雄般地设法将my 97 lines reproducer简化为更简单的44 lines reproducer,我进一步简化为35 lines

using u64 = unsigned long long;

struct Bucket {
    u64 mLeaves[16] = {};
};

struct BucketMap {
    u64 acquire() noexcept {
        while (true) {
            u64 map = mData;

            u64 index = (map & 1) ? 1 : 0;
            auto mask = u64(1) << index;

            auto previous =
                __atomic_fetch_or(&mData, mask, __ATOMIC_SEQ_CST);
            if ((previous & mask) == 0) {
                return index;
            }
        }
    }

    __attribute__((noinline)) Bucket acquireBucket() noexcept {
        acquire();
        return Bucket();
    }

    volatile u64 mData = 1;
};

int main() {
    BucketMap map;
    map.acquireBucket();
    return 0;
}

哪个生成以下程序集:

BucketMap::acquireBucket():
  mov r8, rdi
  mov rdx, rsi

.L2:
  mov rax, QWORD PTR [rsi]
  xor eax, eax
  lock bts QWORD PTR [rdx], rax
  setc al
  jc .L2
  mov rdi, r8
  mov ecx, 16
  rep stosq
  mov rax, r8
  ret

main:
  sub rsp, 152
  lea rsi, [rsp+8]
  lea rdi, [rsp+16]
  mov QWORD PTR [rsp+8], 1
  call BucketMap::acquireBucket()
  xor eax, eax
  add rsp, 152
  ret

xor eax,eax意味着此处的程序集始终尝试获取索引0 ...导致无限循环。

对于该程序集,我只能看到两种解释:

  1. 我以某种方式触发了未定义行为。
  2. gcc中存在代码生成错误。

我已经用尽了所有关于触发UB的想法。

谁能解释为什么gcc会生成此xor eax,eax

注意:暂时向gcc报告为https://gcc.gnu.org/bugzilla/show_bug.cgi?id=86314


使用的编译器版本:

$ gcc --version
gcc (GCC) 7.3.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is 
NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR 
PURPOSE.

编译器标志:

-Wall -Wextra -Werror -Wduplicated-cond -Wnon-virtual-dtor -Wvla 
-rdynamic -Wno-deprecated-declarations -Wno-type-limits 
-Wno-unused-parameter -Wno-unused-local-typedefs -Wno-unused-value 
-Wno-aligned-new -Wno-implicit-fallthrough -Wno-deprecated 
-Wno-noexcept-type -Wno-register -ggdb -fno-strict-aliasing 
-std=c++17 -Wl,--no-undefined -Wno-sign-compare 
-g -O3 -mpopcnt

1 实际上,它比我预期的要好,编译器理解fetch_or(bit(index))后跟previous & bit(index)等同于使用bts和检查CF标志是否为纯金。

3 个答案:

答案 0 :(得分:5)

这是gcc中的窥孔优化错误,请参阅影响版本7.1、7.2、7.3和8.1的#86413。该修补程序已经存在,将分别在7.4版和8.2版中提供。


简短的答案是,特定的代码序列(fetch_or +检查结果)会生成setcc(根据标志的状态设置为有条件),后跟movzbl(移动和零扩展);在7.x中引入了一种优化,该优化将setccmovzbl转换为xorsetcc,但是此优化缺少一些检查,导致{{ 1}}可能破坏仍然需要的寄存器(在这种情况下为xor)。


更长的答案是,eax可以完全实现为fetch_or的实现,或者,如果仅将一位设置为cmpxchg(进行位测试和设置),则可以实现。作为7.x中引入的另一个优化,gcc现在在此处生成bts(gcc 6.4仍生成bts)。 cmpxchg将进位标志(bts)设置为该位的先前值。

也就是说,CF将生成:

  • auto previous = a.fetch_or(bit); auto n = previous & bit;设置该位并捕获其先前值,
  • lock bts QWORD PTR [<address of a>], <bit index>setc <n>l的低8位设置为进位标志(r<n>x)的值,
  • CFmovzx e<n>x, <n>l的高位清零。

然后将应用窥孔优化,这会使事情变得混乱

gcc干线现在会生成proper assembly

r<n>x

尽管不幸的是,优化不再适用,所以我们只剩下BucketMap::acquireBucket(): mov rdx, rdi mov rcx, rsi .L2: mov rax, QWORD PTR [rsi] and eax, 1 lock bts QWORD PTR [rcx], rax setc al movzx eax, al jc .L2 mov rdi, rdx mov ecx, 16 rep stosq mov rax, rdx ret main: sub rsp, 152 lea rsi, [rsp+8] lea rdi, [rsp+16] mov QWORD PTR [rsp+8], 1 call BucketMap::acquireBucket() xor eax, eax add rsp, 152 ret + setc而不是mov + xor ...但至少是正确的!

答案 1 :(得分:1)

作为旁注,您可以通过简单的位操作找到最低的0位:

template<class T>
T find_lowest_0_bit_mask(T value) {
    T t = value + 1;
    return (t ^ value) & t;
}

返回位掩码,而不是位索引。

先决条件:T必须是无符号的,value必须包含至少1个零位。


mData.load必须与mData.fetch_or同步,因此应该是

mData.load(std::memory_order_acquire)

mData.fetch_or(..., std::memory_order_release)

而且,IMO,关于这些位内在函数的某些问题使它使用clang生成错误的汇编,请参见.LBB0_5 loop that is clearly wrong,因为它一直在尝试设置相同的位,而不是重新计算要设置的另一位。生成correct assembly的版本:

#include <cstdint>
#include <atomic>

static constexpr int NO_INDEX = -1;

template<class T>
T find_lowest_0_bit_mask(T value) {
    T t = value + 1;
    return (t ^ value) & t;
}

class AtomicBitMap {
public:
    static constexpr std::uint64_t occupied() noexcept { return ~std::uint64_t(0); }

    int acquire() noexcept {
        auto map = mData.load(std::memory_order_acquire);
        while(map != occupied()) {
            std::uint64_t mask = find_lowest_0_bit_mask(map);
            if(mData.compare_exchange_weak(map, map | mask, std::memory_order_release))
                return __builtin_ffsl(mask) - 1;
        }
        return NO_INDEX;
    }

    void release(int i) noexcept {
        mData.fetch_and(~bit(i), std::memory_order_release);
    }

private:
    static constexpr std::uint64_t bit(int index) noexcept { 
        return std::uint64_t(1) << index; 
    }

    std::atomic_uint64_t mData{ 0 };
};

答案 2 :(得分:0)

xor-zero /设置标志/ setcc通常是创建32位0/1整数的最佳方法。

很明显,只有当备用寄存器为xor-零而又不破坏标志设置指令的任何输入时,这样做才是安全的。显然,这是一个错误。

(否则,您可以setcc dl / movzx eax,dl。最好使用单独的reg,这样movzx在某些CPU上可以是零延迟(消除运动),但是在其他CPU上却是关键路径,因此最好使用xor / set-flags / setcc习惯用语,因为关键路径上的指令较少。)

IDK为什么gcc会在寄存器中完全创建(previous & mask) == 0的整数值;这可能是错误的一部分。