zoukankan      html  css  js  c++  java
  • 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及以上就可以了)

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

    完。

  • 相关阅读:
    HDU1029 Ignatius and the Princess IV
    UVA11039 Building designing【排序】
    UVA11039 Building designing【排序】
    POJ3278 HDU2717 Catch That Cow
    POJ3278 HDU2717 Catch That Cow
    POJ1338 Ugly Numbers(解法二)
    POJ1338 Ugly Numbers(解法二)
    UVA532 Dungeon Master
    UVA532 Dungeon Master
    POJ1915 Knight Moves
  • 原文地址:https://www.cnblogs.com/zhangyachen/p/10349061.html
Copyright © 2011-2022 走看看