C++ 最理想的情况是,如果一个变量在多个线程中读取,但只在一个线程中写入,那么它是否应该在写入线程中以非原子方式读取?

C++ 最理想的情况是,如果一个变量在多个线程中读取,但只在一个线程中写入,那么它是否应该在写入线程中以非原子方式读取?,c++,multithreading,c++11,C++,Multithreading,C++11,具体来说,我应该使用此模板读取writer线程中的变量以获得最佳性能吗 template <typename T> inline T load_non_atomic(const std::atomic<T> &v) { if (sizeof(std::atomic<T>) == sizeof(T)) return(* reinterpret_cast<const T *>(&v)); else

具体来说,我应该使用此模板读取writer线程中的变量以获得最佳性能吗

template <typename T>
inline T load_non_atomic(const std::atomic<T> &v)
  {
    if (sizeof(std::atomic<T>) == sizeof(T))
      return(* reinterpret_cast<const T *>(&v));
    else
      return(v.load(std::memory_order_relaxed));
  }
模板
内联T加载非原子(常量标准::原子&v)
{
if(sizeof(std::atomic)=sizeof(T))
返回(*重新解释铸件(&v));
其他的
返回(v.load(std::memory_order_released));
}

不,您描述的是未定义的行为

一个好的优化器会将一个原子读取减少为一个读取,如果这样做是定义行为的话。您可能没有一个像样的优化器,或者您定义的行为代码提出的问题比您实际需要的更严格


如果您这样做,您现在就负责审核生成的程序集、生成的机器代码、CPU和内存体系结构,在以后的每一次代码编译中,跨越操作系统修订、编译器版本更新、硬件更改等

所以,如果你的代码要编译一次,运行一次,然后扔掉,你所做的只是一个荒谬的工作量

如果它将有更长的生命周期,那么您所做的几乎是不可估量的努力,以避免在将来某个日期代码库中出现随机中断


在没有大量证据的情况下执行此操作会生成更快的代码(没有证据),更快的代码是正确的,并且速度的提高对您的问题至关重要,这简直太愚蠢了。

你不能像现在这样通过
重新解释
在指针上将
std::atomic
强制转换为
t
对象,尽管你经常会发现它在实践中是有效的

除了UB之外,主要的缺点是编译器不必在每次调用此方法时重新加载值,这可能是您想要的。您可能会发现它只是缓存了值,打破了底层算法所做的假设(例如,如果您在循环中检查标志,则该值可能永远看不到变化)

实际上,
v.load(std::memory\u order\u relaxed)
无论如何都会在大多数平台上生成快速代码

例如,以下读取两个
std::atomic
的代码几乎与您的hack使用的plan
.load()
编译得一样好:

模板

inline T load_cheating(const std::atomic<T> &v) {
  return (* reinterpret_cast<const T *>(&v));
}

template <typename T>
inline T load_relaxed(const std::atomic<T> &v) {
  return (v.load(std::memory_order_relaxed));
}

int add_two_cheating(const std::atomic<int> &a, const std::atomic<int> &b) {
  return load_cheating(a) + load_cheating(b);
}

int add_two_relaxed(const std::atomic<int> &a, const std::atomic<int> &b) {
  return load_relaxed(a) + load_relaxed(b);
}
内联加载欺骗(const std::atomic&v){
返回(*重新解释铸件(&v));
}
模板
内联T加载(常数标准::原子和v){
返回(v.load(std::memory_order_released));
}
int add_two_欺骗(const std::atomic&a,const std::atomic&b){
返回加载作弊(a)+加载作弊(b);
}
int add_two_relaxed(常数std::原子&a,常数std::原子&b){
返回荷载(a)+荷载(b);
}
这两个版本最终为:

add_two_cheating(std::atomic<int> const&, std::atomic<int> const&):
        mov     eax, DWORD PTR [rsi]
        add     eax, DWORD PTR [rdi]
        ret
添加两个欺骗(std::atomic const&,std::atomic const&):
mov eax,DWORD PTR[rsi]
添加eax、DWORD PTR[rdi]
ret

add_two_relaxed(std::atomic const&,std::atomic const&):
mov edx,DWORD PTR[rdi]
mov eax,DWORD PTR[rsi]
添加eax、edx
ret
它们具有基本相同的性能1。也许有一天,后者将是相同的,尽管从最实际的角度来看,它已经是相同的

即使在内存模型较弱的ARM上,您也要支付零性能成本:

add_two_cheating(std::atomic<int> const&, std::atomic<int> const&):
        ldr     w2, [x0]
        ldr     w0, [x1]
        add     w0, w2, w0
        ret

add_two_relaxed(std::atomic<int> const&, std::atomic<int> const&):
        ldr     w0, [x0]
        ldr     w1, [x1]
        add     w0, w1, w0
        ret
添加两个欺骗(std::atomic const&,std::atomic const&):
ldr w2[x0]
ldr w0,[x1]
加上w0,w2,w0
ret
添加两个(标准::原子常数&,标准::原子常数&):
ldr w0[x0]
ldr w1,[x1]
添加w0、w1、w0
ret
两个地方产生的代码相同(或多或少RISC ARM体系结构没有load op指令,因此您看不到在x86上所做的细微差别)

注意即使在同一个线程上,一旦使用类型双关指针读取或修改变量,即使是单线程代码也可能被破坏(例如,读取可能会忽略以前的写入,或者在某些情况下,读取可能会看到将来在同一线程上发生的写入)

查看
三重非原子
示例-它们都将单线程行为弄错了。我不容易通过一个插入的
std::atomic.store()
类型操作实现这一点,可能是因为这些操作在今天没有得到优化(即使是宽松的顺序似乎也意味着编译器的障碍),但它们肯定会在将来实现



在现代x86上,未使用域中的操作数相同,延迟也可能相同,但第一个在融合域中的uop确实少了一个。我们平均取周期差的一小部分,如果有的话。

我觉得第一个条件的目的是什么。“我应该使用此模板读取writer线程中的变量以获得最佳性能吗?”否!一千次,不!听起来像是共享互斥的用例。也不要去掉原子代码。如果类型自然是原子的(如x86上的int),那么代码将得到优化。您想在那里编写代码以强制执行总顺序。@Jarod42如果您的意思是reinterpret_cast在标准下永远不能被视为可移植的,我认为这是正确的。这试图解决什么问题?您没有注意到这个模板仅用于在编写它的线程中读取变量。所以一个改变是不能错过的。@WaltK-很公平,虽然它肯定会因为别名而丢失-当后续读取通过错误类型的指针进行时,编译器不需要考虑对变量的写入。@BeeOnRope关于类型双关指针和重新排序的有趣注释。@walt-
triple\u作弊
在我尝试的编译器上起作用。
nomatomic
是坏的,这些例子表明,由于这种类型转换,他们今天已经得到了错误的答案,并且在
add_two_cheating(std::atomic<int> const&, std::atomic<int> const&):
        ldr     w2, [x0]
        ldr     w0, [x1]
        add     w0, w2, w0
        ret

add_two_relaxed(std::atomic<int> const&, std::atomic<int> const&):
        ldr     w0, [x0]
        ldr     w1, [x1]
        add     w0, w1, w0
        ret