gpt4 book ai didi

c++ - 按需条件 std::atomic_thread_fence 获取的优缺点?

转载 作者:塔克拉玛干 更新时间:2023-11-03 02:08:35 25 4
gpt4 key购买 nike

下面的代码显示了两种通过原子标志获取共享状态的方法。读取器线程调用 poll1()poll2() 来检查写入器是否已发出标志。

投票选项#1:

bool poll1() {
return (flag.load(std::memory_order_acquire) == 1);
}

投票选项#2:

bool poll2() {
int snapshot = flag.load(std::memory_order_relaxed);
if (snapshot == 1) {
std::atomic_thread_fence(std::memory_order_acquire);
return true;
}
return false;
}

请注意,选项 #1 是 presented in an earlier question ,选项 #2 类似于 example code at cppreference.com .

假设读者同意仅在 poll 函数返回 true 时检查共享状态,那么这两个 poll 函数是否正确且等价?

选项 #2 是否有标准名称?

每个选项的优点和缺点是什么?

选项 #2 在实践中可能更有效吗?是否有可能降低效率?

这是一个完整的工作示例:

#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>

int x; // regular variable, could be a complex data structure

std::atomic<int> flag { 0 };

void writer_thread() {
x = 42;
// release value x to reader thread
flag.store(1, std::memory_order_release);
}

bool poll1() {
return (flag.load(std::memory_order_acquire) == 1);
}

bool poll2() {
int snapshot = flag.load(std::memory_order_relaxed);
if (snapshot == 1) {
std::atomic_thread_fence(std::memory_order_acquire);
return true;
}
return false;
}

int main() {
x = 0;

std::thread t(writer_thread);

// "reader thread" ...
// sleep-wait is just for the test.
// production code calls poll() at specific points

while (!poll2()) // poll1() or poll2() here
std::this_thread::sleep_for(std::chrono::milliseconds(50));

std::cout << x << std::endl;

t.join();
}

最佳答案

我想我可以回答你的大部分问题。

这两个选项当然是正确的,但它们并不完全等同,因为独立围栏的适用范围略广(它们在你想要完成的事情方面是等价的,但独立围栏在技术上可以适用于还有其他事情——想象一下如果这段代码是内联的)。 this post by Jeff Preshing 中解释了独立围栏与存储/获取围栏有何不同的示例。 .

据我所知,选项 #2 中的先检查后围栏模式没有名称。不过,这并不少见。

就性能而言,在 x64 (Linux) 上使用我的 g++ 4.8.1,两个选项生成的程序集归结为单个加载指令。这不足为奇,因为 x86(-64) 加载和存储无论如何都在硬件级别具有获取和释放语义(x86 以其非常强大的内存模型而闻名)。

但是,对于 ARM,内存屏障编译为实际的单个指令,会产生以下输出(使用 gcc.godbolt.com-O3 -DNDEBUG):

对于 while (!poll1());:

.L25:
ldr r0, [r2]
movw r3, #:lower16:.LANCHOR0
dmb sy
movt r3, #:upper16:.LANCHOR0
cmp r0, #1
bne .L25

对于 while (!poll2());:

.L29:
ldr r0, [r2]
movw r3, #:lower16:.LANCHOR0
movt r3, #:upper16:.LANCHOR0
cmp r0, #1
bne .L29
dmb sy

您可以看到,唯一的区别是同步指令 (dmb) 的放置位置——在 poll1 的循环内,在 poll2 的循环之后。所以 poll2 在这种真实情况下确实更有效 :-)(但请进一步阅读,了解为什么如果在循环中调用它们以阻塞直到标志更改,这可能无关紧要。)

对于 ARM64,输出是不同的,因为有内置障碍的特殊加载/存储指令(ldar -> load-acquire)。

对于 while (!poll1());:

.L16:
ldar w0, [x1]
cmp w0, 1
bne .L16

对于 while (!poll2());:

.L24:
ldr w0, [x1]
cmp w0, 1
bne .L24
dmb ishld

同样,poll2 导致一个循环,内部没有障碍,外部有一个障碍,而 poll1 每次通过时都会有一个障碍。

现在,实际上哪个性能更好需要运行基准测试,不幸的是我没有相应的设置。 poll1poll2,与直觉相反,在这种情况下最终可能同样有效,因为花费额外的时间等待内存效应在循环内传播可能实际上并没有浪费时间如果标志变量是无论如何都需要传播的那些效果之一(即,即使对 poll1 的单个(内联)调用比对 轮询 2)。当然,这是假设一个等待标志更改的循环——对 poll1 的单独调用 do 比对 poll2 的单独调用需要更多的工作.

所以,我认为总的来说,可以肯定地说 poll2 的效率永远不会比 poll1 低得多,而且通常会更快,只要编译器可以消除内联时的分支(这似乎至少是这三种流行架构的情况)。

我的(略有不同的)测试代码供引用:

#include <atomic>
#include <thread>
#include <cstdio>

int sharedState;
std::atomic<int> flag(0);

bool poll1() {
return (flag.load(std::memory_order_acquire) == 1);
}

bool poll2() {
int snapshot = flag.load(std::memory_order_relaxed);
if (snapshot == 1) {
std::atomic_thread_fence(std::memory_order_acquire);
return true;
}
return false;
}

void __attribute__((noinline)) threadFunc()
{
while (!poll2());
std::printf("%d\n", sharedState);
}

int main(int argc, char** argv)
{
std::thread t(threadFunc);
sharedState = argc;
flag.store(1, std::memory_order_release);
t.join();
return 0;
}

关于c++ - 按需条件 std::atomic_thread_fence 获取的优缺点?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35271393/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com