内嵌汇编语言比本地C++代码慢吗?

15 浏览
0 Comments

内嵌汇编语言比本地C++代码慢吗?

我尝试比较内联汇编语言和C++代码的性能,所以我写了一个函数,对大小为2000的两个数组进行100000次相加操作。以下是代码:\n

#define TIMES 100000
void calcuC(int *x,int *y,int length)
{
    for(int i = 0; i < TIMES; i++)
    {
        for(int j = 0; j < length; j++)
            x[j] += y[j];
    }
}
void calcuAsm(int *x,int *y,int lengthOfArray)
{
    __asm
    {
        mov edi,TIMES
        start:
        mov esi,0
        mov ecx,lengthOfArray
        label:
        mov edx,x
        push edx
        mov eax,DWORD PTR [edx + esi*4]
        mov edx,y
        mov ebx,DWORD PTR [edx + esi*4]
        add eax,ebx
        pop edx
        mov [edx + esi*4],eax
        inc esi
        loop label
        dec edi
        cmp edi,0
        jnz start
    };
}

\n以下是`main()`函数:\n

int main() {
    bool errorOccured = false;
    setbuf(stdout,NULL);
    int *xC,*xAsm,*yC,*yAsm;
    xC = new int[2000];
    xAsm = new int[2000];
    yC = new int[2000];
    yAsm = new int[2000];
    for(int i = 0; i < 2000; i++)
    {
        xC[i] = 0;
        xAsm[i] = 0;
        yC[i] = i;
        yAsm[i] = i;
    }
    time_t start = clock();
    calcuC(xC,yC,2000);
    //    calcuAsm(xAsm,yAsm,2000);
    //    for(int i = 0; i < 2000; i++)
    //    {
    //        if(xC[i] != xAsm[i])
    //        {
    //            cout<<"xC["<Debug   Release
---------------
732        668
733        680
659        672
667        675
684        694
Average:   677

\n

C++版本的函数:

\n

Debug     Release
-----------------
1068      168
 999      166
1072      231
1002      166
1114      183
Average:  182

\n在发布模式下,C++代码几乎比汇编代码快3.7倍。为什么呢?\n我猜测我写的汇编代码不如GCC生成的代码高效。对于像我这样的普通程序员来说,很难比编译器生成的代码更快。这是否意味着我不应该相信自己写的汇编语言的性能,而应该专注于C++,忘记汇编语言呢?

0
0 Comments

在深入探讨汇编之前,存在着更高级别的代码转换。

static int const TIMES = 100000;
void calcuC(int *x, int *y, int length) {
  for (int i = 0; i < TIMES; i++) {
    for (int j = 0; j < length; j++) {
      x[j] += y[j];
    }
  }
}

可以通过Loop Rotation进行转换,得到:

static int const TIMES = 100000;
void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      for (int i = 0; i < TIMES; ++i) {
        x[j] += y[j];
      }
    }
}

从内存局部性的角度来看,这种转换效果更好。

这可以进一步优化,因为`a += b` X次等价于`a += X * b`,所以我们有:

static int const TIMES = 100000;
void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      x[j] += TIMES * y[j];
    }
}

然而,我的最喜欢的优化器(LLVM)似乎没有执行这个转换。

编辑后,我发现如果我们给`x`和`y`加上`restrict`限定符,就会执行这个转换。事实上,如果没有这个限定符,`x[j]`和`y[j]`可能会指向同一位置,这样转换就是错误的。

无论如何,这是我认为优化后的C版本。已经简化了很多。基于这个,下面是我尝试的汇编代码(我让Clang生成的,我对汇编一窍不通):

calcuAsm: #

.Ltmp0:

.cfi_startproc

# BB#0:

testl %edx, %edx

jle .LBB0_2

.align 16, 0x90

.LBB0_1: # %.lr.ph

# =>This Inner Loop Header: Depth=1

imull $100000, (%rsi), %eax # imm = 0x186A0

addl %eax, (%rdi)

addq $4, %rsi

addq $4, %rdi

decl %edx

jne .LBB0_1

.LBB0_2: # %._crit_edge

ret

.Ltmp1:

.size calcuAsm, .Ltmp1-calcuAsm

.Ltmp2:

.cfi_endproc

恐怕我不明白所有这些指令是从哪里来的,但你可以尝试一下看看它的性能如何…不过我仍然会在代码中使用优化后的C版本,因为它更具可移植性。

谢谢你的回答。嗯,有点令人困惑的是,当我上《编译原理》课时,我学到编译器会通过许多手段来优化我们的代码。这是否意味着我们需要手动优化我们的代码?我们能比编译器做得更好吗?这个问题一直困扰着我。

我们可以在拥有更多信息时进行更好的优化。具体来说,在这里阻碍编译器的是`x`和`y`之间可能的别名。也就是说,编译器无法确定对于所有`i, j`在`[0, length)`范围内,是否存在`x + i != y + j`。如果存在重叠,那么优化就是不可能的。C语言引入了`restrict`关键字,告诉编译器两个指针不能别名,但是对数组不起作用,因为即使它们不完全别名,它们仍然可以重叠。

当前的GCC和Clang会自动向量化(在省略`__restrict`的情况下检查是否存在重叠)。SSE2是x86-64的基准,通过重新排列可以一次执行2次32位乘法(产生64位乘积,因此需要重新排列将结果重新组合)。GCC有一个巧妙的技巧,可以将常数整数乘法器转换为移位/加法(和/或减法),对于具有少量比特位的乘法器来说非常好。Clang的依赖于重新排列的代码将在Intel CPU上的重新排列吞吐量上出现瓶颈。

0
0 Comments

汇编语言是否比本地C++代码慢?

汇编代码不够优化,可以改进的地方如下:

  • 在内层循环中,你正在推送和弹出一个寄存器(EDX)。这应该移到循环外。
  • 你在每次循环迭代中重新加载数组指针。这应该移到循环外。
  • 你使用loop指令,这在大多数现代CPU上被认为是非常慢的(可能是使用古老汇编书的结果)。
  • 你没有利用手动循环展开。
  • 你没有使用可用的SIMD指令。

所以,除非你大大提高汇编语言的技能,否则没有必要为了性能而编写汇编代码。

*当然,我不知道你是否真的是从一本古老的汇编书中得到了loop指令。但你几乎从不在实际的代码中看到它,因为每个编译器都足够聪明,不会生成loop指令,你只会在我认为是糟糕和过时的书中看到它。

如果你优化大小,编译器可能仍然会生成loop(和许多"已弃用"指令)。

是的,但原始问题确切地涉及速度,而不是大小。

0
0 Comments

在C++和C中,低级语言(汇编语言)总是比高级语言(C++和C)产生更快的代码,这是一个错误的假设。编写代码的方式和对架构细节的了解极大地影响性能。编译器可以进行优化,优化的方式包括寄存器分配、常量传播、公共子表达式消除、指令调度等复杂且不明显的优化。现代的CISC CPU也有非常长的流水线,RISC架构的CPU也无需手动调整指令调度。对于一些复杂的微控制器,甚至系统库也是用C语言编写的,因为编译器可以生成更好(且易于维护)的最终代码。编译器有许多优化选项,可以在编译时自动使用一些MMX/SIMDx指令。对于循环,编译器通常会进行循环优化,比如循环展开。总之,编译器能够进行更多的优化和自动化,而且现代编译器已经非常强大。因此,大部分情况下,汇编语言比本地C++代码更慢。此外,编写汇编语言需要考虑不同的CPU架构,这会增加复杂性和维护成本。因此,在绝大多数情况下,重新设计算法和应用高级优化是更好的选择,而不是转换为汇编语言。

0