为什么GCC会将字复制到返回寄存器而不是字节?

时间:2014-03-04 03:12:34

标签: c gcc optimization x86-64

是否存在逻辑上的原因GCC(4.4.7)没有直接将字节从结构移动到%eax,还是仅仅是优化监督?

考虑以下计划:

struct foo { unsigned char x; };
struct bar { unsigned int x; };

int foo (const struct foo *x, int y) { return x->x * y; }
int bar (const struct bar *x, int y) { return x->x * y; }

使用GCC进行编译时,foo()bar()的差异比预期的大得多:

foo:
.LFB0:
        .cfi_startproc
        movzbl  (%rdi), %edx
        movl    %esi, %eax
        imull   %edx, %eax
        ret
        .cfi_endproc

bar:
.LFB1:
        .cfi_startproc
        movl    (%rdi), %eax
        imull   %esi, %eax
        ret
        .cfi_endproc

我预计foo()会像bar()一样,除非使用不同的移动指令。

我会注意到在clang-500.2.79下,编译器生成了我期望的foo()代码,而foo()bar()具有相同数量的指令(就像我一样)对GCC也有预期,但错了)。

2 个答案:

答案 0 :(得分:2)

由于你在函数foo中乘以uchar x和uint y,编译器必须首先将uchar x提升为int,这是指令movzbl所做的。

请参阅the explanation of movz instructions here.

之后我用gcc 4.6.1和-O3选项重新编译了你的代码,我得到了如下组合:

foo:
.LFB34:
    .cfi_startproc
    movzbl  (%rdi), %eax
    imull   %esi, %eax
    ret 
    .cfi_endproc

bar:
.LFB35:
    .cfi_startproc
    movl    (%rdi), %eax
    imull   %esi, %eax
    ret 
    .cfi_endproc

它不再使用%edx。

答案 1 :(得分:2)


简短的回答

  

为什么GCC会将字复制到返回寄存器而不是字节?

因为你要求它返回一个字而不是一个字节。编译器根据您的代码完成了他们的要求。您要求在一个案例中进行大小提升,并在两个案例中签名都未签名。有不止一种方法可以做到这一点,而clang / llvm和gcc恰好有所不同。

  

是否存在逻辑上的原因GCC(4.4.7)没有直接将结构中的字节移动到%eax中,还是仅仅是优化疏忽?

我认为根据我们在当前编译器中看到的内容,这是一个疏忽。请参阅下面的生成代码(每种情况都使用-O2)。


与此问题相关的有趣实验。

0000000000000000 <foo>:
   0:   0f b6 07                movzbl (%rdi),%eax
   3:   0f af c6                imul   %esi,%eax
   6:   c3                      retq   

0000000000000010 <bar>:
  10:   0f af 37                imul   (%rdi),%esi
  13:   89 f0                   mov    %esi,%eax
  15:   c3                      retq   

GCC

0000000000000000 <foo>:
   0:   0f b6 07                movzbl (%rdi),%eax
   3:   0f af c6                imul   %esi,%eax
   6:   c3                      retq   

0000000000000010 <bar>:
  10:   8b 07                   mov    (%rdi),%eax
  12:   0f af c6                imul   %esi,%eax
  15:   c3                      retq   

他们都生成了正确的代码。对于这个指令集上的这些小函数,指令字节数的微小差异实际上已经消失了。

当时你的编译器出于某种原因一定不能看到这种优化。

MIPS:

00000000 <foo>:
   0:   90820000    lbu v0,0(a0)
   4:   00000000    nop
   8:   00450018    mult    v0,a1
   c:   00001012    mflo    v0
  10:   03e00008    jr  ra
  14:   00000000    nop

00000018 <bar>:
  18:   8c820000    lw  v0,0(a0)
  1c:   00000000    nop
  20:   00a20018    mult    a1,v0
  24:   00001012    mflo    v0
  28:   03e00008    jr  ra
  2c:   00000000    nop

00000000 <foo>:
   0:   e5d00000    ldrb    r0, [r0]
   4:   e0000091    mul r0, r1, r0
   8:   e12fff1e    bx  lr

0000000c <bar>:
   c:   e5900000    ldr r0, [r0]
  10:   e0000091    mul r0, r1, r0
  14:   e12fff1e    bx  lr

没有像x86那样的大惊喜,不同之处在于加载以及它如何处理其他24位然后代码表示它将unsigned char或int提升为有符号整数然后乘以并返回有符号整数。

另一个同样有趣的例子来补充你的问题。

struct foo { unsigned char x; };
struct bar { unsigned int x; };

char foo (const struct foo *x, char y) { return x->x * y; }
char bar (const struct bar *x, char y) { return x->x * y; }

0000000000000000 <foo>:
   0:   8a 07                   mov    (%rdi),%al
   2:   40 f6 e6                mul    %sil
   5:   0f be c0                movsbl %al,%eax
   8:   c3                      retq   

0000000000000010 <bar>:
  10:   0f af 37                imul   (%rdi),%esi
  13:   40 0f be c6             movsbl %sil,%eax
  17:   c3                      retq   

GCC

0000000000000000 <foo>:
   0:   89 f0                   mov    %esi,%eax
   2:   f6 27                   mulb   (%rdi)
   4:   c3                      retq   

0000000000000010 <bar>:
  10:   89 f0                   mov    %esi,%eax
  12:   f6 27                   mulb   (%rdi)
  14:   c3                      retq   

gcc arm

00000000 <foo>:
   0:   e5d00000    ldrb    r0, [r0]
   4:   e0010190    mul r1, r0, r1
   8:   e20100ff    and r0, r1, #255    ; 0xff
   c:   e12fff1e    bx  lr

00000010 <bar>:
  10:   e5900000    ldr r0, [r0]
  14:   e0010190    mul r1, r0, r1
  18:   e20100ff    and r0, r1, #255    ; 0xff
  1c:   e12fff1e    bx  lr

MIPS

00000000 <foo>:
   0:   90820000    lbu v0,0(a0)
   4:   00052e00    sll a1,a1,0x18
   8:   00052e03    sra a1,a1,0x18
   c:   00a20018    mult    a1,v0
  10:   00001012    mflo    v0
  14:   00021600    sll v0,v0,0x18
  18:   03e00008    jr  ra
  1c:   00021603    sra v0,v0,0x18

00000020 <bar>:
  20:   8c820000    lw  v0,0(a0)
  24:   00052e00    sll a1,a1,0x18
  28:   00052e03    sra a1,a1,0x18
  2c:   00a20018    mult    a1,v0
  30:   00001012    mflo    v0
  34:   00021600    sll v0,v0,0x18
  38:   03e00008    jr  ra
  3c:   00021603    sra v0,v0,0x18

该代码特别惩罚了mips。

最后

struct foo { unsigned char x; };
struct bar { unsigned int x; };

unsigned char foo (const struct foo *x, unsigned char y) { return x->x * y; }
unsigned char bar (const struct bar *x, unsigned char y) { return x->x * y; }

x86的gcc和clang使用非指定的字符生成与上面相同的代码,但是

00000000 <foo>:
   0:   e5d00000    ldrb    r0, [r0]
   4:   e0010190    mul r1, r0, r1
   8:   e20100ff    and r0, r1, #255    ; 0xff
   c:   e12fff1e    bx  lr

00000010 <bar>:
  10:   e5900000    ldr r0, [r0]
  14:   e0010190    mul r1, r0, r1
  18:   e20100ff    and r0, r1, #255    ; 0xff
  1c:   e12fff1e    bx  lr

MIPS

00000000 <foo>:
   0:   90820000    lbu v0,0(a0)
   4:   30a500ff    andi    a1,a1,0xff
   8:   00a20018    mult    a1,v0
   c:   00001012    mflo    v0
  10:   03e00008    jr  ra
  14:   304200ff    andi    v0,v0,0xff

00000018 <bar>:
  18:   8c820000    lw  v0,0(a0)
  1c:   30a500ff    andi    a1,a1,0xff
  20:   00a20018    mult    a1,v0
  24:   00001012    mflo    v0
  28:   03e00008    jr  ra
  2c:   304200ff    andi    v0,v0,0xff

由于调用约定和指令集的组合,需要屏蔽。对这两个指令集的惩罚......当使用大小与这些指令集的寄存器大小不匹配的变量时,你会经常看到这一点。 x86具有更广泛的指令选择,x86的成本是额外逻辑成本的功率(瓦​​特)。

对于笑容,即使你回过头来,寄存器大小的选择也会稍微便宜一些。

00000000 <_foo>:
   0:   1166            mov r5, -(sp)
   2:   1185            mov sp, r5
   4:   9f40 0004       movb    *4(r5), r0
   8:   45c0 ff00       bic $-400, r0
   c:   1001            mov r0, r1
   e:   7075 0006       mul 6(r5), r1
  12:   1040            mov r1, r0
  14:   1585            mov (sp)+, r5
  16:   0087            rts pc

00000018 <_bar>:
  18:   1166            mov r5, -(sp)
  1a:   1185            mov sp, r5
  1c:   1d41 0006       mov 6(r5), r1
  20:   707d 0004       mul *4(r5), r1
  24:   1040            mov r1, r0
  26:   1585            mov (sp)+, r5
  28:   0087            rts pc

编译器版本

gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1
Copyright (C) 2013 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

clang --version
clang version 3.4 (branches/release_34 201060)
Target: x86_64-unknown-linux-gnu
Thread model: posix

arm-none-eabi-gcc --version
arm-none-eabi-gcc (GCC) 4.8.2
Copyright (C) 2013 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

mips-elf-gcc --version
mips-elf-gcc (GCC) 4.8.2
Copyright (C) 2013 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

最后一个指令集对于读者来说是一个练习,在拆卸中有一点线索......