ARM 上的 std::atomic<bool> 无锁不一致(树莓派 3)
Posted
技术标签:
【中文标题】ARM 上的 std::atomic<bool> 无锁不一致(树莓派 3)【英文标题】:std::atomic<bool> lock-free inconsistency on ARM (raspberry pi 3) 【发布时间】:2020-10-07 10:23:11 【问题描述】:我遇到了静态断言的问题。静态断言是这样的:
static_assert(std::atomic<bool>::is_always_lock_free);
并且代码在 Raspberry Pi 3 上失败(Linux raspberrypi 4.19.118-v7+ #1311 SMP Mon Apr 27 14:21:24 BST 2020 armv7l GNU/Linux)。
在cppreference.com atomic::is_always_lock_free reference 网站上声明:
如果此原子类型始终是无锁的,则等于 true;如果它从不或有时无锁,则等于 false。 该常量的值与宏 ATOMIC_xxx_LOCK_FREE 一致,其中定义了成员函数 is_lock_free 和非成员函数 std::atomic_is_lock_free。
对我来说第一个奇怪的事情是“有时无锁”。它取决于什么?但问题稍后,回到问题。
我做了一个小测试。写了这段代码:
#include <iostream>
#include <atomic>
int main()
std::atomic<bool> dummy ;
std::cout << std::boolalpha
<< "ATOMIC_BOOL_LOCK_FREE --> " << ATOMIC_BOOL_LOCK_FREE << std::endl
<< "dummy.is_lock_free() --> " << dummy.is_lock_free() << std::endl
<< "std::atomic_is_lock_free(&dummy) --> " << std::atomic_is_lock_free(&dummy) << std::endl
<< "std::atomic<bool>::is_always_lock_free --> " << std::atomic<bool>::is_always_lock_free << std::endl;
return 0;
使用g++ -std=c++17 atomic_test.cpp && ./a.out
(g++ 7.3.0 和 8.3.0,但这不重要)在 raspberry 上编译并运行它并得到:
ATOMIC_BOOL_LOCK_FREE --> 1
dummy.is_lock_free() --> true
std::atomic_is_lock_free(&dummy) --> true
std::atomic<bool>::is_always_lock_free --> false
如您所见,它与 cppreference 网站上所述不一致...作为比较,我在笔记本电脑 (Ubuntu 18.04.5) 上使用 g++ 7.5.0 运行它并得到:
ATOMIC_BOOL_LOCK_FREE --> 2
dummy.is_lock_free() --> true
std::atomic_is_lock_free(&dummy) --> true
std::atomic<bool>::is_always_lock_free --> true
所以ATOMIC_BOOL_LOCK_FREE
的值和is_always_lock_free
常量当然存在差异。寻找ATOMIC_BOOL_LOCK_FREE
的定义我能找到的是
c++/8/bits/atomic_lockfree_defines.h: #define ATOMIC_BOOL_LOCK_FREE __GCC_ATOMIC_BOOL_LOCK_FREE
c++/8/atomic: static constexpr bool is_always_lock_free = ATOMIC_BOOL_LOCK_FREE == 2;
ATOMIC_BOOL_LOCK_FREE
(或__GCC_ATOMIC_BOOL_LOCK_FREE
)等于1或2有什么区别?如果 1 那么它可能是无锁的,也可能不是无锁的,如果 2 是 100% 无锁的?除了0还有其他值吗?这是 cppreference 站点上的一个错误,其中声明所有这些返回值都应该是一致的?树莓派输出的哪个结果是真的?
【问题讨论】:
【参考方案1】:ATOMIC_xxx_LOCK_FREE
宏表示:
0
用于永远不会无锁的内置原子类型
1
用于有时是无锁的内置原子类型
2
用于始终无锁的内置原子类型。
因此,在您的 PI 环境中,std::atomic<bool>
有时是无锁的,而您正在测试的 dummy
实例是无锁的 - 这意味着 所有 实例是。
bool std::atomic_is_lock_free( const std::atomic<T>* obj )
:
在任何给定的程序执行中,无锁查询的结果对于相同类型的所有指针都是相同的。
唯一的缺点是在你运行程序之前你不知道类型是否是无锁的。
If(not std::atomic_is_lock_free(&dummy))
std::cout << "Sorry, the program will be slower than expected\n";
【讨论】:
最有可能的是,“有时无锁”的意思是“在编译时不知道是无锁的”,但运行时查询函数一旦在 RPi 上运行就会返回 true凭借其 ARMv7 CPU。可能使用-march=native
或-mcpu=cortex-whatever
你会得到is_always_lock_free 是true
。 (没有那个编译选项,std::atomic 操作必须调用 libatomic
函数,所以即使在现代 CPU 上也会有额外的开销)。 GCC(和所有健全的编译器)实现std::atomic<T>
的方式,它要么对所有实例无锁,要么无锁,不检查对齐或在每个实例的运行时进行任何操作。
PeterCordes:这似乎是合理的。 @Rogus 你可以试试-m
选项,看看你是否让它始终无锁?
我在编译中添加了-mcpu=cortex-a53
,它成功了! ATOMIC_BOOL_LOCK_FREE
是 2 并且 std::atomic<bool>::is_always_lock_free
评估为 true
!非常感谢您的帮助!【参考方案2】:
1
在标准中表示“有时无锁”。 但这实际上意味着“不知道在编译时是无锁的”。
如果没有编译器选项,GCC 的默认基线包括 ARM 芯片太旧以至于它们不支持原子 RMW 的必要指令,因此它必须制作可以在古老的 CPU 上运行的代码,总是调用 libatomic 函数而不是内联原子操作.
当您在具有 ARMv7 或 ARMv8 CPU 的 RPi 上运行运行时查询函数时,它会返回 true。
使用-march=native
或-mcpu=cortex-a53
你会得到is_always_lock_free
为真,因为在编译时知道目标机器肯定支持所需的指令。 (这些选项告诉 GCC 生成一个可能无法在其他/旧 CPU 上运行的二进制文件。)这是confirmed by the OP in comments。
如果没有该编译选项,std::atomic
操作必须调用 libatomic 函数,因此即使在现代 CPU 上也会有额外的开销。
GCC(和所有健全的编译器)实现std::atomic<T>
的方式,它要么对所有实例无锁,要么无锁,不检查对齐或在运行时每个对象的任何内容。
alignof( std::atomic<int64_t> )
是 8,即使 alignof( int64_t )
在 32 位机器上只有 4,所以如果你有一个未对齐的原子对象,这是未定义的行为。 (对于纯加载和纯存储,UB 的实际症状可能包括撕裂,即非原子性。)如果您遵循 C++ 规则,则所有原子对象都将对齐;如果您将未对齐的指针投射到 atomic<int64_t> *
并尝试使用它,您只会遇到问题。
【讨论】:
为 RISC-V 编译时会怎样? @AaronFranke:如果 LL/SC 指令是 RISC-V 的基线,则 IDK。如果是这样,那么即使没有任何拱形选项,is_always_lock_free
也是正确的。您可以随时使用 RISC-V GCC 或 clang 在 godbolt.org 上试用它。以上是关于ARM 上的 std::atomic<bool> 无锁不一致(树莓派 3)的主要内容,如果未能解决你的问题,请参考以下文章
std::atomic<std::string> 是不是正常工作?
C++ error: use of deleted function ‘std::atomic<short unsigned int>::atomic(const std::atomic<short