【问题标题】:Unexpected x64 assembly for __atomic_fetch_or with gcc 7.3带有 gcc 7.3 的 __atomic_fetch_or 的意外 x64 程序集
【发布时间】:2018-12-03 20:32:47
【问题描述】:

我正在尝试使用 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

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


使用的编译器版本:

$ 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 &amp; bit(index)相当于使用bts并检查CF标志是纯的黄金。

【问题讨论】:

  • 你能提供一个最小的更大的程序吗?抱歉 :) 但这确实看起来很奇怪,因为 tzcnt 的结果被移动到 rcx 中,然后不使用了。您是否尝试过其他编译器版本?
  • @Jester:rcx 的结果将在以后使用(在指令+288 处),因此保存它确实有意义。我还没有设法制作 MVCE,如果我这样做了,我会更新问题。我没有另一个版本的 gcc 可以实际编译代码,这就是为什么我试图在 Godbolt 上重现它。
  • 据我所知,xor eax, eax 进一步与setc al 相关联,但乍一看,我不明白为什么编译器认为它需要它。除了它似乎是由FlatSlotMap::acquireBucket 中的if (bucketIndex == NO_INDEX) 检查触发的(删除if 块也摆脱了无法解释的代码)。奇数。
  • 实际上将第 28 行更改为 BitMap mLocalLeaves[x]; 会在 x &gt;= 11 中断,这是当归零从单独的 SSE 商店切换到 rep stosq 时。
  • @Jester:记录 GCC 错误 gcc.gnu.org/bugzilla/show_bug.cgi?id=86314

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


【解决方案1】:

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


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


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

auto previous = a.fetch_or(bit); auto n = previous &amp; bit;会生成:

  • lock bts QWORD PTR [&lt;address of a&gt;], &lt;bit index&gt; 设置位,并捕获其先前的值,
  • setc &lt;n&gt;lr&lt;n&gt;x的低8位设置为进位标志的值(CF),
  • movzx e&lt;n&gt;x, &lt;n&gt;lr&lt;n&gt;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 而不是xor + setc...但至少它是正确的!

【讨论】:

  • 但是为什么会有setc呢?填充mLeaves[16]rep stosqlock bts 具有数据依赖性是没有意义的。 (BTS 设置的标志上的 jc 只有在 rax 为 0 时才会失效,所以它实际上并没有错,但是在循环之后正常的 xor eax,eax 将是获得零的更好方法。)源代码只是 acquire(); return Bucket(); 但 gcc 在 acquire 循环中添加代码以从 CF 创建一个 0/​​1 寄存器,因此 Bucket() 将有一个 0
  • 也许lock bts 窥视孔吞噬了本来会在 reg 中自然留下 0 的代码。
  • 报告Bug 86352。我可能应该报告 5 个单独的错误,这些错误是关于这个代码生成的所有不好的东西,即使它们都来自这个函数。 ://
  • @PeterCordes:感谢您的关注,正确性是更紧迫的问题,但现在确实有理由不提高性能!
  • @PeterCordes:我很遗憾您的错误没有得到更多关注。我很高兴立即拾取错误的代码生成器(考虑到它们的危险性),但至少看到对报告的问题的一些确认会很好:(
【解决方案2】:

附带说明,您可以通过直接的位操作找到最低 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^n - 1 形式的数字。例如,对于 7 (0b111),预期答案是 3 或 4(取决于这是否为 0 - 基于或基于 1)。相反,7 ^ 8 产生 0b1111 与 7 相加产生 8。
  • @MatthieuM 对于7,预期的答案是8
  • 哇,第一个 Clang 代码是一团糟,它甚至截断了 NO_INDEX
  • @MatthieuM.:bts 效率不高:疯狂的 CISC 位串语义允许它在寻址模式选择的内存操作数之外进行索引,需要大量的微指令。 (不确定这对于lock 版本有多大意义。对于带有内存操作数的非locked 版本来说很重要)。因此,最好编写一个手动 OR + cmpxchg 循环来使用desired = old | old+1; 设置最低位,并使用ctz 找出 哪个 位。 x86 在硬件中没有fetch_or;如果你使用结果,它只能用lock cmpxchglock bts 来做。
  • @PeterCordes:我想知道,还没有查看 Agner 的指南。我的用例的主要优点是lock bts 允许并发“释放”(设置为0),而lock cmpxchg 在这种情况下会失败,我认为这是一个不错的属性。就基准而言,它虽然在噪音中迷失了方向,但损失并不大。
【解决方案3】:

xor-zero/set flags/setcc 通常是创建 32 位 0/1 整数的最佳方式。

显然,只有当您有一个备用寄存器到xor-zero 且不破坏标志设置指令的任何输入时,这样做才是安全的,所以这显然是一个错误。

(否则您可以setcc dl / movzx eax,dl。单独的 reg 更可取,因此 movzx 在某些 CPU 上可以是零延迟(mov-elimination),但它在其他 CPU 上位于关键路径上,因此 xor/set- flags / setcc idiom 更可取,因为关键路径上的指令更少。)

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

【讨论】:

    猜你喜欢
    • 2013-01-07
    • 1970-01-01
    • 1970-01-01
    • 2017-08-20
    • 1970-01-01
    • 1970-01-01
    • 2023-03-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多