本文同时发表在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变量放到了寄存器上。

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

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

完。

golang的加法比C快?的更多相关文章

  1. golang学习

    1. 学习资源列表 https://github.com/golang/go/wiki 2. 最快的入门方法 直接通过代码学习 https://tour.go-zh.org 3. go指南 https ...

  2. C语言代码优化(转)

    .选择合适的算法和数据结构 选择一种合适的数据结构很重要,如果在一堆随机存放的数中使用了大量的插入和删除指令,那使用链表要快得多.数组与指针语句具有十分密切的关系,一般来说,指针比较灵活简洁,而数组则 ...

  3. 【转载】C代码优化方案

    C代码优化方案 1.选择合适的算法和数据结构2.使用尽量小的数据类型3.减少运算的强度 (1)查表(游戏程序员必修课) (2)求余运算 (3)平方运算 (4)用移位实现乘除法运算 (5)避免不必要的整 ...

  4. go 开发中需要注意的与python的不同点

    从python转golang开发已经3个月了,因为写过c++,所以对golang接受的还算快,这段经历也不是很痛苦.伯乐在线上看了一些大神关于python转golang过程中的不适应和吐槽,决定写下篇 ...

  5. 嵌入式C编程代码优化笔记

    [优化永远是追求某种平衡而不是走极端,优化是有侧重点的,优化是一门平衡的艺术,它往往要以牺牲程序的可读性或者增加代码长度为代价] 1.选择合适的算法和数据结构 选择一种合适的数据结构很重要,如果在一堆 ...

  6. Rust这种新型的语言注定火不起来,功能太强大(特性太多),还不如用成熟稳定强大的C/C++,而且生态不行、所以恶性循环

    这种新型的语言注定火不起来,功能太强大(特性太多),还不如用成熟稳定强大的C/C++,,而Golang足够简单,入门快,编译快,性能也强悍,解决了服务端开发人员的痛点,,注定被大多数人接受... go ...

  7. [TJOI2007] 足彩投注

    足彩投注 题目概述 题目背景 了解足球彩票的人可能知道,足球彩票中有一种游戏叫做"胜负彩",意为猜比赛的胜负.下面是一些与胜负彩有关的术语 注 :每一组有效组合数据. 投 注:彩民 ...

  8. (转)C代码优化方案

    C代码优化方案 原文地址:http://www.uml.org.cn/c++/200811103.asp 目录 C代码优化方案 1.选择合适的算法和数据结构 2.使用尽量小的数据类型 3.减少运算的强 ...

  9. 数学--数论--HDU 6128 Inverse of sum (公式推导论)

    Description 给nn个小于pp的非负整数a1,-,na1,-,n,问有多少对(i,j)(1≤i<j≤n)(i,j)(1≤i<j≤n)模pp在意义下满足1ai+aj≡1ai+1aj ...

随机推荐

  1. 再学UML-Bug管理系统UML2.0建模实例(二)

    2.3 BMS顺序图(需求模型)       在UML中,我们将顺序图分为两类,一类用于描述系统需求,构造系统的需求模型(分析模型):另一类用于指导设计与实现,构造系统的实现模型(设计模型).     ...

  2. MyEclipse2015Stable3.0破解方法

    原理大概是这样的(个人粗略分析):获取当前的日期,来设置证书失效日期,解析后生成码-->再转码,最后生成序列号. 1.新建一个Java工程,(不会安装jdk创建环境变量的,请前往传送门:链接.) ...

  3. Jmeter入门16 数据构造之随机数Random Variable & __Random函数

     接口测试有时参数使用随机数构造.jmeter添加随机数两种方式 1  添加配置 > Random Variable  2  __Random函数   ${__Random(1000,9999) ...

  4. ubuntu误删home目录

    今天第一次写shell脚本,一不小心把home目录全给删除了. 解决方案: 先把手打上二十大板!!! [root@myshell ~]#mkdir /home/test01             / ...

  5. Batch Normalization:Accelerating Deep Network Training by Reducing Internal Covariate Shift(BN)

    internal covariate shift(ics):训练深度神经网络是复杂的,因为在训练过程中,每层的输入分布会随着之前层的参数变化而发生变化.所以训练需要更小的学习速度和careful参数初 ...

  6. Laravel5 打印SQL

    在src/Illuminate/Database/Connection.php里打印SQL默认是关闭的,见https://github.com/laravel/framework/commit/e0a ...

  7. 关于刷新同级layer弹框的解决方法

    在项目中遇到这种情况: 父页面点击详情,layer.open一个子页面A,子页面里面又存在操作按钮,点击使用parent.layer.open在打开一个子页面B,子页面B点击提交操作成功要刷新子页面A ...

  8. Redis分布式锁的正确实现方式(Java版)

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...

  9. Anaconda的使用—Spyder常用快捷键

    Ctrl + 1: 注释/反注释 Ctrl + 4/5: 块注释/块反注释 Ctrl + L: 跳转到行号 Tab/Shift + Tab: 代码缩进/反缩进 Ctrl +I:显示帮助

  10. Rman 管理 archivelog 的命令

    因为archivelog的相关信息是记录在controlfile中的,当物理删除后不会改变controlfile的设置.并且在查询相关的动态视图(例如v$archived_log)时,该部分日志仍然标 ...