GCC 生成的 64 位代码比 32 位代码慢三倍。

8

我注意到我的代码在64位Linux上运行比在32位Linux或64位Windows或64位Mac上慢得多。 这是一个最小化的测试案例。

#include <stdlib.h>

typedef unsigned char UINT8;

void
stretch(UINT8 * lineOut, UINT8 * lineIn, int xsize, float *kk)
{
    int xx, x;

    for (xx = 0; xx < xsize; xx++) {
        float ss = 0.0;
        for (x = 0; x < xsize; x++) {
            ss += lineIn[x] * kk[x];
        }
        lineOut[xx] = (UINT8) ss;
    }
}

int
main( int argc, char** argv )
{
    int i;
    int xsize = 2048;

    UINT8 *lineIn = calloc(xsize, sizeof(UINT8));
    UINT8 *lineOut = calloc(xsize, sizeof(UINT8));
    float *kk = calloc(xsize, sizeof(float));

    for (i = 0; i < 1024; i++) {
        stretch(lineOut, lineIn, xsize, kk);
    }

    return 0;
}

以下是它的运行方式:

$ cc --version
cc (Ubuntu 4.8.2-19ubuntu1) 4.8.2
$ cc -O2 -Wall -m64 ./tt.c -o ./tt && time ./tt
user  14.166s
$ cc -O2 -Wall -m32 ./tt.c -o ./tt && time ./tt
user  5.018s

正如您所看到的,32位版本运行速度几乎快了3倍(我在32位和64位Ubuntu上进行了测试,结果相同)。更奇怪的是性能取决于C标准:

$ cc -O2 -Wall -std=c99 -m32 ./tt.c -o ./tt && time ./tt
user  15.825s
$ cc -O2 -Wall -std=gnu99 -m32 ./tt.c -o ./tt && time ./tt
user  5.090s

这怎么可能?我该如何解决这个问题,以加速由GCC生成的64位版本。

更新1

我比较了快速32位(默认和gnu99)和慢速(c99)产生的汇编代码,发现以下问题:

.L5:
  movzbl    (%ebx,%eax), %edx   # MEM[base: lineIn_10(D), index: _72, offset: 0B], D.1543
  movl  %edx, (%esp)    # D.1543,
  fildl (%esp)  #
  fmuls (%esi,%eax,4)   # MEM[base: kk_18(D), index: _72, step: 4, offset: 0B]
  addl  $1, %eax    #, x
  cmpl  %ecx, %eax  # xsize, x
  faddp %st, %st(1) #,
  fstps 12(%esp)    #
  flds  12(%esp)    #
  jne   .L5 #,

在快速情况下,没有fstpsflds命令。因此,GCC在每个步骤中从内存中存储和加载值。我尝试了register float类型,但这并没有帮助。

更新2

我已经在gcc-4.9上进行了测试,看起来它为64位生成了最佳代码。而-ffast-math(由@jch建议)修复了-m32 -std=c99对于两个GCC版本。我仍在寻找解决方案,以便在gcc-4.8上实现64位,因为现在它比4.9更常见。


3的因子表明自动向量化失败了。查看-S汇编清单。 - Hans Passant
@HansPassant 不是的,“矢量化由标志 -ftree-vectorize 启用,并在默认情况下在 -O3 下启用”。 - homm
当我在没有使用GCC优化的情况下运行程序时,我得到了大约14.349秒(x86)VS 14.723秒(x86_64)的执行时间。GCC优化失败了吗? - Bechir
不涉及问题,但为了提供信息:您不能定义名为“stretch()”的函数,所有以“str”开头的公共函数名称都被保留。 - unwind
@unwind 此作品中出现的所有人物均为虚构。任何与真实人物,无论是活着的还是已故的,都是纯属巧合。 - homm
1
你有检查过libc6的版本,是否编译为64位架构,并且用于编译64位版本吗? - user3629249
4个回答

9

旧版本的GCC生成的代码存在部分依赖停顿。

movzbl (%rsi,%rax), %r8d
cvtsi2ss %r8d, %xmm0  ;; all upper bits in %xmm0 are false dependency

依赖关系可以通过 "xorps" 打破。
#ifdef __SSE__
float __attribute__((always_inline)) i2f(int v) {
    float x;
    __asm__("xorps %0, %0; cvtsi2ss %1, %0" : "=x"(x) : "r"(v) );
    return x;
}
#else
float __attribute__((always_inline)) i2f(int v) { return (float) v; }
#endif

void stretch(UINT8* lineOut, UINT8* lineIn, int xsize, float *kk)
{
    int xx, x;

    for (xx = 0; xx < xsize; xx++) {
        float ss = 0.0;
        for (x = 0; x < xsize; x++) {
            ss += i2f(lineIn[x]) * kk[x];
        }
        lineOut[xx] = (UINT8) ss;
    }
}

结果

$ cc -O2 -Wall -m64 ./test.c -o ./test64 && time ./test64
./test64  4.07s user 0.00s system 99% cpu 4.070 total
$ cc -O2 -Wall -m32 ./test.c -o ./test32 && time ./test32
./test32  3.94s user 0.00s system 99% cpu 3.938 total

哇,这在我的项目中可以运行,不仅在测试用例中。唯一的问题是:这段代码如何跨平台?always_inline会破坏任何编译器吗?所有x86 64位目标CPU在编译期间都有__SSE__标志吗?此外,我注意到i2f应该是静态的。 - homm
@homm 嗯,我猜如果你用正确的ifdefs来保护它(可惜我不能立刻告诉你),它将是跨平台的 :) 我预计这已经可以在Mac / Linux上使用GCC和clang构建(即使clang不真正需要此hack,它也会自行打破依赖关系)。您需要在MSVC中将#else分支中的__attribute__更改为__forceinline,但否则它仍应该工作,因为MSVC 定义__SSE__ - Vyacheslav Egorov
如果我要将这段代码包含到我的项目中,我可能会这样写:#if (GCC_VERSION < 40900) && defined(__SSE__) - Vyacheslav Egorov
请注意,在使用旧版 GCC 的系统上,也可能存在不了解这些指令的汇编程序。我在 py-Pillow 包中的代码中遇到了 libImaging/ImagingUtils.h:40: Error: operand type mismatch for 'xorps'libImaging/ImagingUtils.h:40: Error: operand type mismatch for 'cvtsi2ss' 错误提示,显然是由于该包采用了这一技巧。 - Rhialto supports Monica
原始代码存在错误,导致您看到了错误@Rhialto,它应该是=x而不是=X。寄存器约束错误。 - Vyacheslav Egorov

2
在32位模式下,编译器会额外努力保持严格的IEEE 754浮点语义。您可以通过使用-ffast-math编译来避免这种情况:
$ gcc -m32 -O2 -std=c99 test.c && time ./a.out 

real    0m13.869s
user    0m13.884s
sys     0m0.000s
$ gcc -m32 -O2 -std=c99 -ffast-math test.c && time ./a.out 

real    0m4.477s
user    0m4.480s
sys     0m0.000s

我无法在64位模式下重现你的结果,但我非常有信心-ffast-math可以解决你的问题。更一般地说,除非你真的需要可重现的IEEE 754舍入行为,否则-ffast-math就是你想要的。


我猜你正在使用gcc-4.9,看起来它为64位生成快速代码,但对于没有使用“-ffast-math”的“-std=c99”仍然很慢。在gcc-4.8上,快速数学运算对“-m64”也没有帮助。但是你的回答仍然非常有帮助。谢谢! - homm
是的,你说得对。我可以使用gcc-4.8重现你的结果,而且确实,无论是-ffast-math还是-march=corei7都不能解决这个问题。 - jch

2
这是我尝试过的方法:我将ss声明为volatile,这样可以防止编译器对其进行优化。在32位和64位版本中,我得到了类似的时间。

64位略慢是正常的,因为64位代码较大,uCode缓存有限。所以总体上,64位应该比32位稍微慢一些(<3-4%)。

回到问题本身,我认为在32位模式下,编译器对ss进行更加积极的优化。

更新 1:

查看64位代码,它生成了一个CVTTSS2SI指令,与一个CVTSI2SS指令配对,用于浮点转整数。这个操作延迟更高。32位代码直接使用FMULS指令,在浮点数上直接操作。需要寻找编译器选项来防止这些转换。


我不能将它声明为 int,我需要使用 float - homm

1

看起来这是一个限制条件的情况。这三个数组不能重叠,对吧?


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