• 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及以上就可以了)

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

    完。

  • 相关阅读:
    Hadoop源码分析1: 客户端提交JOB
    《分布式系统原理与范型》习题答案 6.一致性和复制
    《分布式系统原理与范型》习题答案 5.同步
    《分布式系统原理与范型》习题答案 4.命名
    《分布式系统原理与范型》习题答案 3.进程
    《分布式系统原理与范型》习题答案 2.通信
    《分布式系统原理与范型》习题答案 1.绪论
    计算机基础知识面试
    机器学习面试题
    计算机网络面试题
  • 原文地址:https://www.cnblogs.com/zhangyachen/p/10349061.html
Copyright © 2020-2023  润新知