以下所有说明都做同样的事情:设置%eax为零.哪种方式最佳(需要最少的机器周期)?
xorl %eax, %eax
mov $0, %eax
andl $0, %eax
Run Code Online (Sandbox Code Playgroud) C++ 11标准定义了一个内存模型(1.7,1.10),它包含内存排序,大致是"顺序一致","获取","消费","释放"和"放松".同样粗略地说,程序只有在没有竞争的情况下才是正确的,如果所有动作都可以按照某个动作的顺序放在另一个动作之前,则会发生这种情况.动作 X发生的方式 - 在动作Y 之前是X在Y之前(在一个线程内)被排序,或者X在线程间 - 在Y之前发生.其中,后者的条件是在何时
当X是在某个原子变量上具有"释放"排序的原子存储时发生同步,并且Y是在同一变量上具有"获取"排序的原子加载.作为依赖项,订购前发生了类似的情况,其中Ÿ是负载"消费"排序(和合适的内存访问).的概念下同步,与延伸的之前发生关系传递性跨越的行动被测序,之前彼此线程内,但被相关性排序,之前只有通过严格的子集传递性扩展测序,之前叫依赖于大规模规则的依赖性,特别是可以被中断std::kill_dependency.
那么,"依赖性排序"概念的目的是什么?与简单的排序 - 先前/同步 -排序相比,它提供了什么优势?由于它的规则更严格,我认为可以更有效地实施.
你能举一个程序的例子,从发布/获取到发布/消费的转换是正确的,并提供一个非平凡的优势吗?何时能std::kill_dependency提供改进?高级别参数会很好,但硬件特定差异的奖励点.
考虑以下玩具示例,尤其是result函数:
#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>
class Worker
{
std::thread th;
std::atomic_bool done = false;
int value = 0;
public:
Worker()
: th([&]
{
std::this_thread::sleep_for(std::chrono::seconds(1));
value = 42;
done.store(true, std::memory_order_release);
}) {}
int result() const
{
return done.load(std::memory_order_acquire) ? value : -1;
}
Worker(const Worker &) = delete;
Worker &operator=(const Worker &) = delete;
~Worker()
{
th.join();
}
};
int main()
{
Worker w;
while (true)
{
int r = w.result();
if (r != -1) …Run Code Online (Sandbox Code Playgroud) c++ multithreading atomic micro-optimization memory-barriers
我一直在阅读内存障碍:软件黑客的硬件视图,这是Paul E. McKenney的一篇非常受欢迎的文章.
本文强调的一点是,像Alpha这样非常微弱有序的处理器可以重新排序依赖负载,这似乎是分区缓存的副作用
论文摘录:
1 struct el *insert(long key, long data)
2 {
3 struct el *p;
4 p = kmalloc(sizeof(*p), GPF_ATOMIC);
5 spin_lock(&mutex);
6 p->next = head.next;
7 p->key = key;
8 p->data = data;
9 smp_wmb();
10 head.next = p;
11 spin_unlock(&mutex);
12 }
13
14 struct el *search(long key)
15 {
16 struct el *p;
17 p = head.next;
18 while (p != &head) {
19 /* BUG ON ALPHA!!! */
20 if (p->key …Run Code Online (Sandbox Code Playgroud) synchronization locking cpu-architecture lock-free memory-barriers
在这种情况下,两个负载会合二为一吗?如果这是依赖于架构的,那么说英特尔的现代处理器会是什么情况?我相信原子负载相当于英特尔处理器中的正常负载。
void run1() {
auto a = atomic_var.load(std::memory_order_relaxed);
auto b = atomic_var.load(std::memory_order_relaxed);
// Some code using a and b;
}
void run2() {
if (atomic_var.load(std::memory_order_relaxed) == 2 && /*some conditions*/ ...) {
if (atomic_var.load(std::memory_order_relaxed) * somevar > 3) {
/*...*/
}
}
}
Run Code Online (Sandbox Code Playgroud)
run1()并且run2()只是使用相同原子变量的两个负载的两个场景。编译器能否将这种两种加载的场景合并为一种加载并重用它?
C++11 规定了六种内存顺序:
typedef enum memory_order {
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst
} memory_order;
Run Code Online (Sandbox Code Playgroud)
https://en.cppreference.com/w/cpp/atomic/memory_order
其中默认值为 seq_cst。
可以通过放宽操作的内存顺序来获得性能提升。但是,这取决于体系结构提供的保护。例如,Intel x86 是一种强大的内存模型,可以保证各种加载/存储组合不会被重新排序。
因此relaxed,acquire和release似乎是寻求在x86额外的性能时所需的唯一排序。
这样对吗?如果没有,是否需要在 x86 上使用consume,acq_rel和seq_cst?
来自链接: 加载/存储宽松原子变量和普通变量有什么区别?
这个回答给我留下了深刻的印象:
使用原子变量解决了这个问题——通过使用原子,即使内存顺序放宽,所有线程都可以保证读取最新的写入值。
今天,我阅读了以下链接:https : //preshing.com/20140709/the-purpose-of-memory_order_consume-in-cpp11/
atomic<int*> Guard(nullptr);
int Payload = 0;
Run Code Online (Sandbox Code Playgroud)
线程1:
Payload = 42;
Guard.store(&Payload, memory_order_release);
Run Code Online (Sandbox Code Playgroud)
线程2:
g = Guard.load(memory_order_consume);
if (g != nullptr)
p = *g;
Run Code Online (Sandbox Code Playgroud)
问题: 我了解到数据依赖会阻止相关指令被重新排序。但我认为这对于确保执行结果的正确性是显而易见的。comsume-release 语义是否存在并不重要。所以我想知道 comsum-release 真的可以。哦,也许它使用数据依赖性来防止指令重新排序,同时确保 Payload 的可见性?
所以
如果我使 1.preventing 指令重新排序 2.确保 Payload 的非原子变量的可见性,是否有可能使用 memory_order_relaxed 获得相同的正确结果:
atomic<int*> Guard(nullptr);
volatile int Payload = 0; // 1.Payload is volatile now
// 2.Payload.assign and Guard.store in order for data dependency
Payload = 42;
Guard.store(&Payload, memory_order_release);
// 3.data Dependency …Run Code Online (Sandbox Code Playgroud) 我读过有关携带依赖关系和依赖关系排序之前,在其定义中使用一个5.1.2.4(p16):
在以下情况下,评估
A在评估之前是依赖顺序的B:—
A对原子对象执行释放操作M,并在另一个线程中B执行消耗操作M并读取以 为首的释放序列中的任何副作用写入的值A,或— 对于某些求值
X,A之前是依存顺序X并X带有对 的依存关系B。
所以我试图制作一个可能有用的例子。就这个:
static _Atomic int i;
void *produce(void *ptr){
int int_value = *((int *) ptr);
atomic_store_explicit(&i, int_value, memory_order_release);
return NULL;
}
void *consume(void *ignored){
int int_value = atomic_load_explicit(&i, memory_order_consume);
int new_int_value = int_value + 42;
printf("Consumed = %d\n", new_int_value);
}
int main(int …Run Code Online (Sandbox Code Playgroud) 根据C++标准:
\n\n\n如果 A 的值用作 B 的操作数,则求值 A 具有对求值 B 的依赖性,除非:
\n\xe2\x80\x94 B 是 std::kill_dependency (29.3) 的任何特化的调用,或者
\n\xe2\x80\x94 A 是内置逻辑 AND(&&,参见 5.14)或逻辑 OR(||,参见 5.15)运算符的左操作数,或者
\n\xe2\x80\x94 A 是条件(?:,参见 5.16)运算符的左操作数,或者
\n\xe2\x80\x94 A 是内置逗号 (,) 运算符的左操作数 (5.18);(...)
\n
我可以理解为什么关系之前排序的依赖项会在kill_dependency调用时停止,但是为什么逻辑AND、OR、逗号等运算符也会破坏依赖链?
\n这是否意味着下面的代码有未定义的行为?
\n//thread1\nint y = 2\natomicVal.store(true);\n\n//thread2 \nauto x = atomicVal.load(std::memory_order_consume);\ncout << x && y;\nRun Code Online (Sandbox Code Playgroud)\n 我在这篇SO帖子中阅读了 [[carries_dependency]] 。
但我无法理解的是接受的答案中的以下句子:
“特别是,如果将使用 memory_order_consume 读取的值传递给函数,然后没有 [[carries_dependency]],那么编译器可能必须发出内存栅栏指令以保证支持适当的内存排序语义。如果参数用 [[carries_dependency]] 注释,那么编译器可以假设函数体将正确携带依赖项,并且可能不再需要此围栏。
类似地,如果一个函数返回一个加载了 memory_order_consume 的值,或者从这样的值派生,那么如果没有 [[carries_dependency]],编译器可能需要插入一个栅栏指令来保证适当的内存排序语义得到支持。有了 [[carries_dependency]] 注释,这个围栏可能不再是必要的,因为调用者现在负责维护依赖树。”
让我们一步一步来:
“如果将使用 memory_order_consume 读取的值传递给函数,然后没有 [[carries_dependency]],那么编译器可能必须发出内存栅栏指令,以确保支持适当的内存排序语义。”
因此,对于释放-消耗内存模型中的原子变量,当原子变量作为参数传递给函数时,编译器将引入栅栏硬件指令,以便它始终具有提供给函数的原子变量的最新和更新值。
下一个 -
“如果参数用 [[carries_dependency]] 注释,那么编译器可以假设函数体将正确携带依赖项,并且可能不再需要这个围栏。”
这让我很困惑 - 原子变量值已经被消耗了,然后函数携带了什么依赖?
相似地 -
“如果一个函数返回一个加载了 memory_order_consume 的值,或者从这样的值派生,那么如果没有 [[carries_dependency]],编译器可能需要插入一个栅栏指令来保证适当的内存排序语义得到支持。使用 [[ Carry_dependency]] 注释,这个围栏可能不再是必要的,因为调用者现在负责维护依赖树。”
从这个例子中,它不清楚它试图说明携带依赖的意义是什么?
c++ ×7
stdatomic ×5
atomic ×4
lock-free ×3
memory-model ×3
x86 ×3
c++11 ×2
optimization ×2
performance ×2
assembly ×1
c ×1
c11 ×1
gcc ×1
locking ×1