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

Sav*_*ife 2 c++ assembly gcc volatile clang

我写了一个简单的循环:

int volatile value = 0;

void loop(int limit) {
  for (int i = 0; i < limit; ++i) { 
      ++value;
  }
}
Run Code Online (Sandbox Code Playgroud)

我用gcc和clang(-O3 -fno-unroll-loops)进行了编译,得到了不同的输出。它们++value部分不同:

铛:

  add dword ptr [rip + value], 1 # ++value
  add edi, -1                    # --limit
  jne .LBB0_1                    # if limit > 0 then continue looping
Run Code Online (Sandbox Code Playgroud)

gcc:

  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
Run Code Online (Sandbox Code Playgroud)

每个编译器上的C和C ++版本都相同(https://gcc.godbolt.org/z/5x5jGP)所以我的问题是:

1)gcc做错了吗?复制的目的是value什么?

2)我已经对该代码进行了基准测试,由于某种原因,探查器显示在gcc版本中,73%的时间浪费在了指令上add edx, 1,13%的时间浪费在了13%的时间mov DWORD PTR value[rip], eaxcmp edi, edx。我把这个结果解释错了吗?为什么其他加法和移动指令所花费的时间不到1%?

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

Pet*_*des 7

这是因为您使用了volatileGCC并没有积极地对其进行优化

如果没有volatile,例如对于单个++*int_ptr,您将获得一个内存目标地址。(希望不是inc在调整Intel CPU时使用;inc reg可以,但是inc mem与添加1相比需要额外的成本。 不幸的是,gcc和clang都弄错了,并inc mem-march=skylakehttps : //godbolt.org/z/_1Ri20一起使用)


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

GCC不知道如何做到这一点的优化volatilevolatile在GCC中使用通常会导致单独的mov加载和存储,从而避免了x86通过将CISC内存操作数用于ALU指令来节省代码大小的能力。在加载/存储计算机(如任何RISC)上,无论如何,您都需要单独的加载和存储指令,因此不会发出。

TL:DR:不同的编译器内部组件 volatile,特别是GCC缺少优化。

由于volatile很少使用,因此错过的优化几乎无关紧要。但是,如果需要,可以随时在GCC的bugzilla中进行报告。

没有volatile,循环当然会优化掉。但是您可以add从GCC或clang中看到一个仅用于此功能的内存目标++*p

1)gcc做错了吗?复制值的目的是什么?

它只是将其复制到寄存器中。通常,我们不称此为“复制”,只是将其放入可以对其进行操作的寄存器中。


请注意,gcc和clang在实现循环条件方面也有所不同,clang仅将其优化为dec / jnz(实际上是add -1,但将dec与-march = skylake或高效的对象dec(即,不是Silvermont)一起使用)。

GCC在循环条件上花费了额外的uop(在add/jnz可以宏融合为单个uop的Intel CPU上)。IDK为什么会这样天真地编译它。

73%的时间浪费在指导上 add edx, 1

性能计数器通常会指责等待缓慢结果的指令,而不是实际上产生结果缓慢的指令。

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

(无论是在内存目标的多个微指令add之间还是在单独的指令之间,都没有任何区别。循环中没有其他内存访问,因此,如果不尝试,存储转发延迟的怪异效果也不会降低很快就会发挥作用: 在没有优化的情况下进行编译或添加具有函数调用的循环时,添加冗余分配可以加速代码,比空循环要快

为什么其他加法和移动指令所花费的时间不到1%?

因为乱序执行将它们隐藏在关键路径的延迟之下。他们是非常罕见的是被指责在统计抽样有挑一出来是在飞行中曾在任何给定的周期中的许多指令。

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

我希望这两个循环都以相同的速度运行。您是说性能是指编译器本身在制作快速紧凑的代码方面的表现吗?