golang的加法比C快?

本文同时发表在https://github.com/zhangyachen/zhangyachen.github.io/issues/142

1.31

晚上的火车回家,在公司还剩两个小时,无心工作,本着不虚度光阴的原则(写这句话时还剩一个半小时~~),还是找点事情干。决定写一下前几天同事遇到的一个golang与c加法速度比较的问题(现在心里在想我工作不饱和的,请大胆的把你的名字放到留言区!)。

操作系统信息:

$uname -a
Linux 35d4aec21d2e 3.10.0-514.16.1.el7.x86_64 #1 SMP Wed Apr 12 15:04:24 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

先看一段C语言的加法:

#include<stdio.h>

int main(){

    long i , sum = 0;

    for ( i = 0 ; i < 9000000000; i++  ) {
        sum += i;
    }

    printf("%ld",sum);

    return 0;
}

执行时间:

$time ./a.out
3606511848080896768
real	0m32.353s
user	0m30.963s
sys	0m1.091s

再看一段GO语言的加法:

package main

import (
        "fmt"
       )

func main() {

    var i, sum uint64
        for i = 0; i < 9000000000; i++ {
            sum += i
        }

    fmt.Print(sum)
}

执行时间:

$time go run a.go
3606511848080896768
real	0m6.272s
user	0m6.142s
sys	0m0.215s

我们可以发现Golang的加法比C版本快5倍以上。结果确实令人大跌眼镜,如果差一点还可以理解,但是这个5倍的差距确实有点大。是什么导致了这种差距?

第一反应肯定是分别查看汇编代码,因为在语言层面实在想不出能有什么因素导致如此大的性能差距,毕竟只是一个加法运算而已。

gcc生成的汇编代码(只看main函数的):

0000000000400540 <main>:
  400540:	55                   	push   %rbp
  400541:	48 89 e5             	mov    %rsp,%rbp
  400544:	48 83 ec 10          	sub    $0x10,%rsp
  400548:	48 c7 45 f0 00 00 00 	movq   $0x0,-0x10(%rbp)  -----> sum = 0
  40054f:	00
  400550:	48 c7 45 f8 00 00 00 	movq   $0x0,-0x8(%rbp)     ---->   i = 0
  400557:	00
  400558:	eb 0d                	jmp    400567 <main+0x27>
  40055a:	48 8b 45 f8          	mov    -0x8(%rbp),%rax     
  40055e:	48 01 45 f0          	add    %rax,-0x10(%rbp)       -------> sum = sum + i
  400562:	48 83 45 f8 01       	addq   $0x1,-0x8(%rbp)        -------> i++
  400567:	48 b8 ff 19 71 18 02 	mov    $0x2187119ff,%rax
  40056e:	00 00 00
  400571:	48 39 45 f8          	cmp    %rax,-0x8(%rbp)         ------> i < 9000000000
  400575:	7e e3                	jle    40055a <main+0x1a>
  400577:	48 8b 45 f0          	mov    -0x10(%rbp),%rax
  40057b:	48 89 c6             	mov    %rax,%rsi
  40057e:	bf 6c 06 40 00       	mov    $0x40066c,%edi
  400583:	b8 00 00 00 00       	mov    $0x0,%eax
  400588:	e8 37 fe ff ff       	callq  4003c4 <printf@plt>
  40058d:	b8 00 00 00 00       	mov    $0x0,%eax     -------> return 0
  400592:	c9                   	leaveq

比较重要的汇编语句已经标记出来了,可以发现,代码中频繁用到的sum和i变量,是放在栈中的(内存),每次运算需要访问内存。

再看看GO编译器生成的汇编代码:

000000000047b660 <main.main>:
  47b660:   64 48 8b 0c 25 f8 ff    mov    %fs:0xfffffffffffffff8,%rcx
  47b667:   ff ff
  47b669:   48 3b 61 10             cmp    0x10(%rcx),%rsp
  47b66d:   0f 86 aa 00 00 00       jbe    47b71d <main.main+0xbd>
  47b673:   48 83 ec 50             sub    $0x50,%rsp
  47b677:   48 89 6c 24 48          mov    %rbp,0x48(%rsp)
  47b67c:   48 8d 6c 24 48          lea    0x48(%rsp),%rbp
  47b681:   31 c0                   xor    %eax,%eax         -----------> i = 0
  47b683:   48 89 c1                mov    %rax,%rcx     ----------->  sum = i =0
  47b686:   48 ba 00 1a 71 18 02    mov    $0x218711a00,%rdx
  47b68d:   00 00 00
  47b690:   48 39 d0                cmp    %rdx,%rax
  47b693:   73 19                   jae    47b6ae <main.main+0x4e>
  47b695:   48 8d 58 01             lea    0x1(%rax),%rbx   ----------->  i++ (1)
  47b699:   48 01 c1                add    %rax,%rcx   -----------> sum = sum + i
  47b69c:   48 89 d8                mov    %rbx,%rax   -----------> i++ (2)
  47b69f:   48 ba 00 1a 71 18 02    mov    $0x218711a00,%rdx
  47b6a6:   00 00 00
  47b6a9:   48 39 d0                cmp    %rdx,%rax
  47b6ac:   72 e7                   jb     47b695 <main.main+0x35>
  47b6ae:   48 89 4c 24 30          mov    %rcx,0x30(%rsp)
  47b6b3:   48 c7 44 24 38 00 00    movq   $0x0,0x38(%rsp)
  47b6ba:   00 00
  47b6bc:   48 c7 44 24 40 00 00    movq   $0x0,0x40(%rsp)
  47b6c3:   00 00
  47b6c5:   48 8d 05 d4 e6 00 00    lea    0xe6d4(%rip),%rax        # 489da0 <type.*+0xdda0>
 47b6cc:   48 89 04 24             mov    %rax,(%rsp)
  47b6d0:   48 8d 44 24 30          lea    0x30(%rsp),%rax
  47b6d5:   48 89 44 24 08          mov    %rax,0x8(%rsp)
  47b6da:   e8 91 02 f9 ff          callq  40b970 <runtime.convT2E>
  47b6df:   48 8b 44 24 10          mov    0x10(%rsp),%rax
  47b6e4:   48 8b 4c 24 18          mov    0x18(%rsp),%rcx
  47b6e9:   48 89 44 24 38          mov    %rax,0x38(%rsp)
  47b6ee:   48 89 4c 24 40          mov    %rcx,0x40(%rsp)
  47b6f3:   48 8d 44 24 38          lea    0x38(%rsp),%rax
  47b6f8:   48 89 04 24             mov    %rax,(%rsp)
  47b6fc:   48 c7 44 24 08 01 00    movq   $0x1,0x8(%rsp)
  47b703:   00 00
  47b705:   48 c7 44 24 10 01 00    movq   $0x1,0x10(%rsp)
  47b70c:   00 00
  47b70e:   e8 4d 90 ff ff          callq  474760 <fmt.Print>
  47b713:   48 8b 6c 24 48          mov    0x48(%rsp),%rbp
  47b718:   48 83 c4 50             add    $0x50,%rsp
  47b71c:   c3                      retq
  47b71d:   e8 ee f7 fc ff          callq  44af10 <runtime.morestack_noctxt>
  47b722:   e9 39 ff ff ff          jmpq   47b660 <main.main>

可以看出,GO编译器将常用的sum和i变量放到了寄存器上。

image

CPU访问寄存器的效率是内存的100倍,是CPU cache的10倍。但是在这个例子中,我猜测大多数情况下应该是cache hit,而不会直接访问内存。

在我的机器环境上,给变量加上register关键字,程序运行时间会有明显的提升:

#include<stdio.h>

int main(){

    //add register keyword
    register long i , sum = 0;

    for ( i = 0 ; i < 9000000000; i++  ) {
        sum += i;
    }

    printf("%ld",sum);

    return 0;
}

执行时间:

$time ./a.out
3606511848080896768
real	0m4.650s
user	0m4.645s
sys	0m0.001s

由之前的32.4秒提升到了4.6秒,效果很明显。
看下生成的汇编:

0000000000400540 <main>:
  400540:	55                   	push   %rbp
  400541:	48 89 e5             	mov    %rsp,%rbp
  400544:	41 54                	push   %r12
  400546:	53                   	push   %rbx
  400547:	41 bc 00 00 00 00    	mov    $0x0,%r12d    ----------> i = 0  lower 32-bit
  40054d:	bb 00 00 00 00       	mov    $0x0,%ebx     -----------> sum = 0
  400552:	eb 07                	jmp    40055b <main+0x1b>
  400554:	49 01 dc             	add    %rbx,%r12     ---------->  sum = sum + i
  400557:	48 83 c3 01          	add    $0x1,%rbx      --------->   i++ 
  40055b:	48 b8 ff 19 71 18 02 	mov    $0x2187119ff,%rax
  400562:	00 00 00
  400565:	48 39 c3             	cmp    %rax,%rbx
  400568:	7e ea                	jle    400554 <main+0x14>
  40056a:	4c 89 e6             	mov    %r12,%rsi
  40056d:	bf 5c 06 40 00       	mov    $0x40065c,%edi
  400572:	b8 00 00 00 00       	mov    $0x0,%eax
  400577:	e8 48 fe ff ff       	callq  4003c4 <printf@plt>
  40057c:	b8 00 00 00 00       	mov    $0x0,%eax
  400581:	5b                   	pop    %rbx
  400582:	41 5c                	pop    %r12
  400584:	5d                   	pop    %rbp
  400585:	c3                   	retq

这时,gcc将变量都放到了寄存器上。

刚才强调了一下在我的机器环境上,因为在我本地的mac上,即使加上regisrer,gcc还是不会将变量放到寄存器上。
我记得K&R里说过,编译器往往比人聪明,不需要我们手动加register关键字,有时候即使加了,编译器也不会把他们放到寄存器上。但是这个例子中,明显将变量放到寄存器会比较好,为什么gcc不这么做呢?有没有高人出来解释一下。(搞明白了,gcc默认是-O0,我一直以为是-O1,如果优化级别是-O1及以上就可以了)

以一篇水文迎接即将到来的新年。

完。

原文地址:https://www.cnblogs.com/zhangyachen/p/10349061.html