C++ C/#x421++;。为什么可以将volatile上的简单整数加法转换为gcc和clang上的不同asm指令?

C++ C/#x421++;。为什么可以将volatile上的简单整数加法转换为gcc和clang上的不同asm指令?,c++,gcc,assembly,clang,volatile,C++,Gcc,Assembly,Clang,Volatile,我写了一个简单的循环: int volatile value = 0; void loop(int limit) { for (int i = 0; i < limit; ++i) { ++value; } } gcc: add dword ptr [rip + value], 1 # ++value add edi, -1 # --limit jne .LBB0_1 # if

我写了一个简单的循环:

int volatile value = 0;

void loop(int limit) {
  for (int i = 0; i < limit; ++i) { 
      ++value;
  }
}
gcc:

  add dword ptr [rip + value], 1 # ++value
  add edi, -1                    # --limit
  jne .LBB0_1                    # if limit > 0 then continue looping
  mov eax, DWORD PTR value[rip] # copy value to a register
  add edx, 1                    # ++i
  add eax, 1                    # increment a copy of value
  mov DWORD PTR value[rip], eax # store incremented copy to value, i. e. ++value
  cmp edi, edx                  # compare i < limit
  jne .L3                       # if i < limit then continue looping
mov eax,DWORD PTR值[rip]#将值复制到寄存器
添加edx,1#++i
添加eax,1#增加一个值副本
mov DWORD PTR值[rip],eax#存储递增拷贝到值,即。e++价值
cmp edi,edx#比较i<极限
如果i

C和C++版本在每个编译程序中都是相同的 因此,我的问题是:

1) gcc是否做错了什么?复制
值的意义是什么

2) 我有那个代码,出于某种原因,分析器显示在gcc的版本中,73%的时间浪费在指令
添加edx,1
,13%浪费在
mov DWORD PTR值[rip],eax
上,13%浪费在
cmp edi,edx
上。我对这个结果的解释是错误的吗?为什么其他添加和移动指令占用的时间少于1%

3) 为什么在这种原始代码中gcc/clang的性能会有所不同?

这都是因为您使用了
volatile
,而gcc没有对其进行积极的优化 如果没有易失性,例如,对于单个
++*int\u ptr
,您将获得一个内存目标添加。(在为英特尔CPU进行调优时,希望不要使用
inc
;不幸的是,gcc和clang都犯了错误,并将
inc mem
-march=skylake
:)


clang知道它可以将
volatile
读/写访问折叠到加载中,并存储内存目标的部分
add

GCC不知道如何对
volatile
进行此优化。在GCC中使用
volatile
通常会导致单独的
mov
加载和存储,从而避免了x86通过对ALU指令使用CISC内存操作数来节省代码大小的能力。在加载/存储机器上(像任何RISC一样),无论如何都需要单独的加载和存储指令,这样就不会出现问题

TL:DR:volatile
周围的不同编译器内部,特别是GCC遗漏的优化

由于很少使用
volatile
,因此错过的优化几乎无关紧要。但如果你愿意,可以在GCC的bugzilla上自由报道

如果没有volatile,循环当然会优化。但是您可以从GCC中看到单个内存目标
add
,或者只执行
++*p
的函数的clang

1) gcc是否做错了什么?复制值有什么意义

它只是把它复制到一个寄存器里。我们通常不称之为“复制”,只是将其放入一个寄存器中,以便对其进行操作


请注意,gcc和clang在实现循环条件的方式上也有所不同,clang只优化到dec/jnz(实际上是
add-1
,但它将使用
dec
和-march=skylake,或者使用高效的
dec
,即不是Silvermont)

GCC在循环条件上花费额外的uop(在英特尔CPU上,
add/jnz
可以将宏融合为单个uop)。IDK为什么它会像那样天真地编译它

73%的时间浪费在指令
添加edx,1

perf计数器通常会责怪等待慢结果的指令,而不是实际生成慢结果的指令

添加edx,1
正在等待重新加载
值。对于4到5个周期的存储转发延迟,这是循环中的主要瓶颈

(无论是在一个内存目标的多个UOP之间
add
,还是在不同的指令之间,本质上都没有区别。循环中没有其他内存访问,因此如果您不尝试过快,存储转发延迟降低的奇怪影响都不会发挥作用: 或)

为什么其他添加和移动指令占用的时间少于1%

因为无序执行将它们隐藏在关键路径的延迟之下。当统计抽样必须在任何给定的周期内同时从众多飞行中挑选一个时,它们很少会受到指责

3) 为什么在如此原始的代码中,gcc/clang的性能会有所不同


我希望这两个环路以相同的速度运行。你指的是性能,是指编译器自己在生成快速紧凑的代码方面的表现吗?

我觉得期望所有编译器生成同等质量的汇编是错误的。输出是不同的,因为它们是不同的编译器,工作方式不同。编译器没有义务生成尽可能最好的代码。他们只会产生最好的代码。@FrançoisAndrieux我不希望他们产生相同的程序集。我不相信在最简单的情况下(整数加法),两个主要的编译器可以产生不同质量的代码。这应该是有原因的@弗朗索瓦·桑德里奥:这有点虚伪。虽然没有任何东西强迫它们完全相同,但您可能希望最流行的编译器之一能够为这样一个简单、常见的构造生成优化良好的代码。@R..:add
指令中有一个加载和存储。C标准没有规定
++value
必须产生单独的加载和存储指令;它说,访问严格按照抽象机器的规则进行评估,但构成对易失性对象的访问的是实现定义的。从内存中加载并存储它可以满足读访问和写访问的要求。@U标记:该弃用建议在源语言级别;优化
volatile\u存储(值,volatile\u加载(值)+1)
到x86上的内存目标加载项仍然是合法的。它甚至不是一个原子RMW,所以存储和加载仍然是分开存在的。(在单芯上)