编译为C++与C时,GCC代码生成存在很大差异

35
我一直在尝试学习有关各种SIMD扩展(MMX,SSE,AVX)的知识,并使用x86-64汇编进行实验。
为了查看GCC如何将不同的C或C ++结构转换为机器代码,我一直在使用Compiler Explorer这个绝妙的工具。
在我的其中一个“游玩会话”中,我想看看GCC如何优化简单的运行时整数数组初始化。 在这种情况下,我尝试将数字0到2047写入包含2048个无符号整数的数组。
代码如下:
unsigned int buffer[2048];

void setup()
{
  for (unsigned int i = 0; i < 2048; ++i)
  {
    buffer[i] = i;
  }
}

如果我启用优化和AVX-512指令-O3 -mavx512f -mtune=intel,GCC 6.3会生成一些非常聪明的代码 :)
setup():
        mov     eax, OFFSET FLAT:buffer
        mov     edx, OFFSET FLAT:buffer+8192
        vmovdqa64       zmm0, ZMMWORD PTR .LC0[rip]
        vmovdqa64       zmm1, ZMMWORD PTR .LC1[rip]
.L2:
        vmovdqa64       ZMMWORD PTR [rax], zmm0
        add     rax, 64
        cmp     rdx, rax
        vpaddd  zmm0, zmm0, zmm1
        jne     .L2
        ret
buffer:
        .zero   8192
.LC0:
        .long   0
        .long   1
        .long   2
        .long   3
        .long   4
        .long   5
        .long   6
        .long   7
        .long   8
        .long   9
        .long   10
        .long   11
        .long   12
        .long   13
        .long   14
        .long   15
.LC1:
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16
        .long   16

然而,当我测试了使用GCC C编译器编译相同代码时添加-x c标志所生成的内容时,我感到非常惊讶。我原以为结果会类似,如果不是完全相同,但C编译器似乎生成了更加复杂和慢速的机器码。生成的汇编代码太长无法在此处粘贴,但可以通过访问godbolt.org并按照this链接查看。下面是生成代码的一部分,从第58行到83行。
.L2:
        vpbroadcastd    zmm0, r8d
        lea     rsi, buffer[0+rcx*4]
        vmovdqa64       zmm1, ZMMWORD PTR .LC1[rip]
        vpaddd  zmm0, zmm0, ZMMWORD PTR .LC0[rip]
        xor     ecx, ecx
.L4:
        add     ecx, 1
        add     rsi, 64
        vmovdqa64       ZMMWORD PTR [rsi-64], zmm0
        cmp     ecx, edi
        vpaddd  zmm0, zmm0, zmm1
        jb      .L4
        sub     edx, r10d
        cmp     r9d, r10d
        lea     eax, [r8+r10]
        je      .L1
        mov     ecx, eax
        cmp     edx, 1
        mov     DWORD PTR buffer[0+rcx*4], eax
        lea     ecx, [rax+1]
        je      .L1
        mov     esi, ecx
        cmp     edx, 2
        mov     DWORD PTR buffer[0+rsi*4], ecx
        lea     ecx, [rax+2]

作为您可以看到的,这段代码有很多复杂的移动和跳跃,并且总体上感觉像是执行简单数组初始化的非常复杂的方法。
为什么生成的代码存在如此大的差异?
在将C和C++都有效的代码进行优化方面,GCC C++编译器是否比C编译器更好?

2
附加数据点:使用 static unsigned int buffer[2048]; 使得 C 代码类似。不过你必须实际使用 buffer,否则它会被完全消除。看起来这是一个对齐问题,额外的代码是为了处理对齐不正确的情况。 - Jester
9
@Olaf,也许您可以告诉我们这段代码在C和C++中语义上的区别。 - M.M
2
@Jester 对于godbolt的专业提示,加入void g(void *); g(buffer);将防止缓冲区被优化掉。 - M.M
5
为什么不应该呢?如果您对gcc在这种情况下如何以及为什么会做出这样的行为有特定见解,请提供一个答案,因为这基本上是OP所问的。 - nos
2
unsigned int buffer[2048] = { 0 };放入代码中也会生成更简单的代码。或许Olaf确实想到了什么,在C语言中,unsigned int buffer[2048]是一个“试探性定义”,而这在C++中是不存在的。这并不会影响程序的可观察行为,但显然它对GCC代码生成有一定影响。 - M.M
显示剩余9条评论
1个回答

41

由于所使用的指令 vmovdqa64 需要 64 字节对齐,因此额外的代码是为了处理不对齐的情况。

我的测试显示,尽管标准没有要求,但在 C 模式下 gcc 允许另一个模块中的定义覆盖此处的定义。该定义可能仅符合基本对齐要求(4 字节),因此编译器不能依赖更大的对齐。从技术上讲,gcc 会为这个试探性定义发出一个 .comm 汇编指令,而外部定义则使用 .data 节中的正常符号。在链接时,这个符号优先于 .comm 符号。

注意,如果将程序更改为使用 extern unsigned int buffer[2048];,那么即使是 C++ 版本也会有额外的代码。相反,将其设置为 static unsigned int buffer[2048]; 将把 C 版本转换为优化版本。


2
注意,如果要使用gcc编译C代码版本,您可以添加“-fno-common”编译器标志,或者在“buffer”变量上注释“attribute((aligned(64)))”,它将生成类似于C++版本的代码。 - nos
1
@M.M 实际上这是一个声明,在 C 中具有外部链接,但在 C++ 中具有内部链接。具有内部链接意味着它必须在此模块中定义,编译器将会完成这个任务。对于 C 来说,它很可能在另一个模块中定义,因此编译器可能需要处理这个问题。当然,添加一个初始化程序将把它转换为定义,并且在 C 中你也不能有更多的定义,所以编译器可以生成优化代码。 - Jester
2
@Jester 在同一翻译单元内才能重新定义尝试性定义。在任何情况下,尝试性定义都是一个定义,而不仅仅是一个声明。在标准C中,你的观点仍然是错误的。 - M.M
2
我越看它,越觉得你是正确的。然而,无论是否有意,gcc允许来自不同模块的初始化程序覆盖试探性定义,并且这将带来自己的对齐方式。鉴于这是未定义行为,因此可能会随后的版本更改,但对于问题中的版本,似乎是这种情况。 - Jester
2
允许在多个翻译单元中定义没有初始化器的内容是Unix的一个扩展功能(这是一个奇怪的概念,因为Unix在ANSI出现之前就有了C)。我相信GNU程序仍在使用此功能。虽然ANSI标准在技术上允许这种用法,但我非常不可能认为GCC会放弃对它的支持。 - Jonathan Cast
显示剩余9条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接