【发布时间】: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...导致无限循环。
对于这个程序集,我只能看到两种解释:
- 我以某种方式触发了未定义行为。
- 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 & 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 >= 11中断,这是当归零从单独的 SSE 商店切换到rep stosq时。 -
@Jester:记录 GCC 错误 gcc.gnu.org/bugzilla/show_bug.cgi?id=86314
标签: c++ gcc assembly x86-64 compiler-bug