优化后的C代码中汇编代码冗余问题

7
我正在尝试通过研究使用-gcc编译的简单C代码并进行-O3优化来学习向量化。更具体地说,我想了解编译器的向量化效果如何。这是一个个人的学习之旅,旨在通过更复杂的计算验证gcc -O3的性能。我知道传统智慧认为编译器比人更好,但我从不把这样的智慧视为理所当然。
然而,在我的第一个简单测试中,我发现gcc做出的一些选择相当奇怪,并且在优化方面非常疏忽。我愿意假设编译器有一些目的,并且知道关于CPU(在这种情况下是Intel i5-2557M)的一些东西,而我不知道。但我需要一些有见识的人的确认。
我的简单测试代码(片段)如下:
int i;
float a[100];

for (i=0;i<100;i++) a[i]= (float) i*i;

对应于for循环的汇编代码(段)如下:

.L6:                        ; loop starts here
    movdqa  xmm0, xmm1      ; copy packed integers in xmm1 to xmm0
.L3:
    movdqa  xmm1, xmm0      ; wait, what!?  WHY!?  this is redundant.
    cvtdq2ps    xmm0, xmm0  ; convert integers to float
    add rax, 16             ; increment memory pointer for next iteration
    mulps   xmm0, xmm0      ; pack square all integers in xmm0
    paddd   xmm1, xmm2      ; pack increment all integers by 4 
    movaps  XMMWORD PTR [rax-16], xmm0   ; store result 
    cmp rax, rdx            ; test loop termination
    jne .L6                 

我理解所有步骤,从计算上讲,所有的都是有意义的。但是,我不明白的是为什么gcc会选择在迭代循环中加入一步将xmm0xmm1交换后再将xmm1加载到xmm0 中的步骤。即:

 .L6
        movdqa  xmm0, xmm1      ; loop starts here
 .L3
        movdqa  xmm1, xmm0      ; grrr! 

这让我怀疑优化器的理智。显然,额外的 MOVDQA 不会干扰数据,但乍一看,gcc的做法似乎极其疏忽。
在汇编代码中的前面部分(未展示),xmm0和xmm2被初始化为在向量化方面具有意义的某些值,所以很明显,在循环开始时,代码必须跳过第一个 MOVDQA。但是,为什么 gcc 不直接像下面展示的那样重新排列呢?
.L3
        movdqa  xmm1, xmm0     ; initialize xmm1 PRIOR to loop
.L6
        movdqa  xmm0, xmm1     ; loop starts here 

甚至更好的方法是直接初始化xmm1,而不是xmm0,然后完全跳过MOVDQA xmm1xmm0步骤!我相信CPU足够聪明,可以跳过冗余步骤,但如果它连这么简单的代码都无法正确优化,我怎么能相信gcc可以完全优化复杂的代码呢?或者有人能提供一个合理的解释,让我相信gcc -O3是好东西吗?

@给踩的人:请评论原因。 - Stefan
1
你确定你的代码比编译器更快吗?你尝试过计时吗? - Degustaf
4
@Stefan 我不是其中一个投反对票的人,但我猜测这是因为这个问题几乎成为了一篇针对gcc的抱怨。 - Degustaf
2
正在使用哪个版本的gcc? - Michael Burr
1
.L3 似乎是一个冗余标签,可能是某个优化过程中剩下的。 (可能是删除了 for 循环的入口条件)所以我并不惊讶其他东西被留下了。毕竟,编译器并不完美。 - Mysticial
显示剩余9条评论
1个回答

4
我不是百分之百确定,但看起来您的循环通过将其转换为浮点数而破坏了xmm0,因此您需要将整数值放入xmm1,然后复制到另一个寄存器中(在这种情况下是xmm0)。
虽然编译器有时会发出不必要的指令,但我实在看不出这种情况。
如果您希望xmm0(或xmm1)保持整数,则不要将i的第一个值强制转换为float。也许您想要做的是:
 for (i=0;i<100;i++) 
    a[i]= (float)(i*i);

但是另一方面,gcc 4.9.2似乎没有这样做:

g++ -S -O3 floop.cpp

.L2:
    cvtdq2ps    %xmm1, %xmm0
    mulps   %xmm0, %xmm0
    addq    $16, %rax
    paddd   %xmm2, %xmm1
    movaps  %xmm0, -16(%rax)
    cmpq    %rbp, %rax
    jne .L2

clang(大约3周前的3.7.0版本)也不行。

 clang++ -S -O3 floop.cpp


    movdqa  .LCPI0_0(%rip), %xmm0   # xmm0 = [0,1,2,3]
    xorl    %eax, %eax
    .align  16, 0x90
.LBB0_1:                                # %vector.body
                                        # =>This Inner Loop Header: Depth=1
    movd    %eax, %xmm1
    pshufd  $0, %xmm1, %xmm1        # xmm1 = xmm1[0,0,0,0]
    paddd   %xmm0, %xmm1
    cvtdq2ps    %xmm1, %xmm1
    mulps   %xmm1, %xmm1
    movaps  %xmm1, (%rsp,%rax,4)
    addq    $4, %rax
    cmpq    $100, %rax
    jne .LBB0_1

我编译的代码:

extern int printf(const char *, ...);

int main()
{
    int i;
    float a[100];

    for (i=0;i<100;i++)
        a[i]= (float) i*i;

    for (i=0; i < 100; i++)
        printf("%f\n", a[i]);
}

(为防止编译器清除所有代码,我添加了printf语句。)

但这实际上就是发生的事情。如果你查看汇编代码,你会发现 xmm0 被转换为浮点数,然后被平方并保存。问题是,为什么编译器在循环跳转后覆盖了 xmm1。 - Marandil
啊,说得好。所以,这只是“编写编译器很难”的另一个案例。如果你想挑战一下自己,我会建议你尝试找出这在gcc中发生的位置,并提出修复方案。 - Mats Petersson
2
或者升级到更高版本的gcc,也许? - Mats Petersson
知道最新版本的gcc更好是很有用的。我没有注意到我正在使用旧版本。而且优化仍在不断改进。 - codechimp
在类型转换的问题上,我添加了(float)以进一步改善编译代码,否则,它会尝试将(i*i)作为更复杂的整数算术运算,并几乎抵消了向量化带来的任何收益。 我也很失望编译器没有提供只将i简单地视为FP的选项,这将简化代码(消除类型转换)。 但是,由于可能对精度产生影响,我愿意对其进行一些限制。 - codechimp
显示剩余3条评论

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