代码之家  ›  专栏  ›  技术社区  ›  Matthieu M.

意外的X64程序集,用于uu Atomic_Fetch_或GCC 7.3

  •  14
  • Matthieu M.  · 技术社区  · 6 年前

    我正在尝试使用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会产生这个吗? 异或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 回复  |  直到 6 年前
        1
  •  5
  •   Matthieu M.    6 年前

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


    简短的答案是特定的代码序列( fetch_or +检查结果)生成 setcc (设置条件,即基于标志状态)后跟 movzbl (移动和零扩展);在7.x中引入了一个优化,它将 国家经贸委 然后 MOVZBL公司 变成 xor 然后 国家经贸委 但是,此优化缺少一些检查,导致 异或 可能会删除一个仍然需要的寄存器(在本例中, eax )。


    更长的答案是 获取\或 可以实现为 cmpxchg 为了充分的通用性,或者,如果只设置一个位,作为 bts (位测试和设置)。作为7.x中引入的另一个优化,gcc现在生成一个 基站 这里(GCC 6.4仍然生成 CMPxCHG公司 )。 基站 设置进位标志( 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 到进位标志的值( 穿越火线 ),请
    • movzx e<n>x, <n>l R<N>X .

    然后应用窥视孔优化,把事情搞砸了。

    GCC主干现在生成 proper assembly 以下内容:

    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 而不是 异或 + 国家经贸委 ……但至少是正确的!

        2
  •  1
  •   Maxim Egorushkin    6 年前

    作为旁注,您可以通过直接向前的位操作找到最低的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)
    

    而且,在我看来,这些位内部函数的某些特性使得它产生了错误的装配 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 };
    };
    
        3
  •  0
  •   Peter Cordes    6 年前

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

    显然,只有当你有一个备用的登记簿 xor -零而不破坏任何对标志设置指令的输入,因此这显然是一个错误。

    (否则你可以 setcc dl / movzx eax,dl .最好使用单独的reg,这样movzx可以在某些CPU上为零延迟(mov消除),但它位于其他CPU上的关键路径上,因此xor/set flags/setcc习惯用法更可取,因为关键路径上的指令更少。)

    idk为什么gcc创建 (previous & mask) == 0 在一个寄存器中,这可能是bug的一部分。