【问题标题】:Why do left operands of logical AND/OR not carry dependency to the parent evaluation?为什么逻辑 AND/OR 的左操作数不依赖于父评估?
【发布时间】:2026-01-06 18:15:01
【问题描述】:

根据 C++ 标准:

如果 - 将 A 的值用作 B 的操作数,则评估 A 对评估 B 具有依赖关系,除非:

— B 是对 std::kill_dependency (29.3) 的任何特化的调用,或

— A 是内置逻辑 AND(&&,参见 5.14)或逻辑 OR(||,参见 5.15)运算符的左操作数,或

——A 是条件 (?:, 见 5.16) 运算符的左操作数,或

— A 是内置逗号 (,) 运算符 (5.18) 的左操作数; (...)

我可以理解为什么在关系之前排序的依赖会在 kill_dependency 调用时停止,但为什么逻辑 AND、OR、逗号等运算符也会破坏依赖链?

这是否意味着下面的代码有未定义的行为?

//thread1
int y = 2
atomicVal.store(true);

//thread2 
auto x = atomicVal.load(std::memory_order_consume);
cout << x && y;

【问题讨论】:

  • 在消费的处理中找不到任何“逻辑”或一致的意图。这是一场火车失事!

标签: c++ atomic lock-free stdatomic carries-dependency


【解决方案1】:

memory_order_consume 试图公开用于 C++ 的 asm 级 CPU 功能。 (它是temporarily deprecated,直到它可以被重新设计为编译器可以在实践中实现的东西,并且在源代码中不需要太多kill_dependency 噪音)。理解 CPU 行为是理解 C++ 设计的关键。

这都是关于数据依赖,而不是像条件分支这样的控制依赖。 C++11: the difference between memory_order_relaxed and memory_order_consume[[carries_dependency]] what it means and how to implement 有更多细节。

例如add x2, x2, x3 指令在其两个输入寄存器都准备好之前无法执行,ldr w1, [x2] 在地址准备好之前也不能执行加载,因此如果 x2 来自另一个加载,它会自动排序在此之前。 (假设 CPU 硬件设计为不违反因果关系,例如通过进行价值预测或 DEC Alpha 在极少数情况下违反因果关系的任何做法)。但是cbz w1, reg_was_zero 可以预测,所以让reg_was_zero: ldr w3, [x4] 等待产生w1 的负载是不够的。 (这是 AArch64 asm,顺便说一句,一种保证依赖排序的弱排序 ISA。)

||left &amp;&amp; right 的短路评估在逻辑上与 if(left) right 相同,因此可以预期分支预测 + 推测执行在右侧运行,即使左侧尚未执行。 没有数据依赖,只有控件依赖。

显然,逗号left, right 根本不会在双方之间建立任何联系,它基本上是将left; right; 塞进一个表达式中的一种方式。

当然,如果你在左右两边使用相同的变量,数据依赖也可以这样存在,但它不是由操作员创建的。

【讨论】:

  • 谢谢彼得,我是否正确理解我分享的 sn-p 在理论上确实是一种未定义的行为,但在实践中是安全的,因为编译器将消费实现为获取?
  • @LifuHuang:y 应该是非原子变量吗?如果线程 1 在存储到 atomicVal 之前执行了y = 2;,则该示例将更有意义。否则,如果没有人修改y,任何线程都可以安全地阅读它!但是是的,如果线程 1 对其进行了修改,然后对 atomic var 进行了发布存储,您至少需要获取来避免数据竞争 UB 的可能性。
最近更新 更多