思考

相信大家在实际的项目开发中会遇到这么一个事,有的程序员写的代码不仅bug少,而且性能高;而有的程序员写的代码能否流畅的跑起来,都是一个很大问题。
而我们今天要讨论的就是一个关于性能优化的案例分析。

案例分析

我们先来构造一些基础数据(长度为10亿的切片,并赋上值):

var testData = GenerateData()

// generate billion slice data
func GenerateData() []int {
data := make([]int, )
for key, _ := range data {
data[key] = key %
} return data
} // get length
func GetDataLen() int {
return len(testData)
}

案例一

// case one
func CaseSumOne(result *int) {
data := GenerateData()
for i := ; i < GetDataLen(); i++ {
*result += data[i]
}
}
// case two
func CaseSumTwo(result *int) {
data := GenerateData()
dataLen := GetDataLen()
for i := ; i < dataLen; i++ {
*result += data[i]
}
}

执行结果

$ go test -bench=.
goos: windows
goarch: amd64
BenchmarkCaseSumOne- ns/op
BenchmarkCaseSumTwo- ns/op
PASS
ok _/C_/go-code/perform/case-one .059s

问题分析

  • CaseSumTwo执行效率是CaseSumOne的2.94倍,快了近三倍,这是为什么呢?
  • 我想这个其实很容易猜到,这里有一个连续的函数调用“GetDataLen()”,

我们来看下两个函数的汇编,做个简单的对比:

函数CaseSumOne

"".CaseSumOne STEXT size= args=0x4 locals=0xc
0x0000 (point.go:) TEXT "".CaseSumOne(SB), $-
...
// point.go:24 -> for i := 0; i < GetDataLen(); i++
0x0021 (point.go:) PCDATA $, $
0x0021 (point.go:) PCDATA $, $
0x0021 (point.go:) MOVL "".result+(SP), DX
0x0025 (point.go:) XORL BX, BX
0x0027 (point.go:) JMP
0x0029 (point.go:) MOVL (CX)(BX*), BP // CX循环计数器
0x002c (point.go:) ADDL BP, (DX)
0x002e (point.go:) INCL BX // i++
0x002f (point.go:) MOVL "".testData+(SB), BP // 栈指针寄存器
0x0035 (point.go:) CMPL BX, BP
0x0037 (point.go:) JGE
...
0x0045 (point.go:) CALL runtime.panicindex(SB)
0x004c (point.go:) CALL runtime.morestack_noctxt(SB)
...

函数CaseSumTwo

"".CaseSumTwo STEXT size= args=0x4 locals=0xc
0x0000 (point.go:) TEXT "".CaseSumTwo(SB), $-
...
// point.go:32 -> dataLen := GetDataLen()
// point.go:33 -> for i := 0; i < dataLen; i++ {
0x0021 (point.go:) MOVL "".testData+(SB), DX
0x0027 (point.go:) PCDATA $, $
0x0027 (point.go:) PCDATA $, $
0x0027 (point.go:) MOVL "".result+(SP), BX
0x002b (point.go:) XORL BP, BP
0x002d (point.go:) JMP
0x002f (point.go:) MOVL (AX)(BP*), SI
0x0032 (point.go:) ADDL SI, (BX)
0x0034 (point.go:) INCL BP
0x0035 (point.go:) CMPL BP, DX
0x0037 (point.go:) JGE
...
0x0045 (point.go:) CALL runtime.panicindex(SB)
0x004c (point.go:) CALL runtime.morestack_noctxt(SB)
...

比较结论

不难发现主要的区别是在CaseSumOne中多了这么一行:

0x002f 00047 (point.go:24) MOVL "".testData+4(SB), BP

其实虽然只有一行,但是对于函数“GetDataLen”里需要调用的指令对CPU的消耗:

"".GetDataLen STEXT size= args=0x4 locals=0x0
0x0000 (point.go:) TEXT "".GetDataLen(SB), $- //
0x0000 (point.go:) MOVL TLS, CX
0x0007 (point.go:) MOVL (CX)(TLS*), CX
0x000d (point.go:) CMPL SP, (CX)
0x0010 (point.go:) JLS
0x0012 (point.go:) FUNCDATA $, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0012 (point.go:) FUNCDATA $, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0012 (point.go:) FUNCDATA $, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0012 (point.go:) PCDATA $, $
0x0012 (point.go:) PCDATA $, $
0x0012 (point.go:) MOVL "".testData+(SB), AX // 寄存器寻址 AX = lenVAL
0x0018 (point.go:) MOVL AX, "".~r0+(SP) // SP = AX = lenVal
0x001c (point.go:) RET
0x001d (point.go:) NOP
0x001d (point.go:) PCDATA $, $-
0x001d (point.go:) PCDATA $, $-
0x001d (point.go:) CALL runtime.morestack_noctxt(SB) // 压栈
...

虽然,看似小小一行代码的区别,但是在指令级的角度上,进行了创建栈空间、压栈、寻址、赋值等一系列操作,况且这里进行了循环调用。

案例二

// case two
func CaseSumTwo(result *int) {
data := GenerateData()
dataLen := GetDataLen()
for i := ; i < dataLen; i++ {
*result += data[i]
}
}
// case three
func CaseSumThree(result *int) {
data := GenerateData()
dataLen := GetDataLen()
tmp := *result
for i:= ; i < dataLen; i++ {
tmp += data[i]
}
*result = tmp
}

执行结果

$ go test -bench=.
goos: windows
goarch: amd64
BenchmarkCaseSumTwo- ns/op
BenchmarkCaseSumThree- ns/op
PASS
ok _/C_/go-code/perform/case-one 8.2773

问题分析

  • 虽然对连续函数调用进行了优化,但是CaseSumThree对执行效率还是高于CaseSumTwo1.52倍,还有哪些情况会影响执行性能呢?

我们再来对比下“CaseSumTwo”和“CaseSumThree”对汇编源码:

函数CaseSumTwo

"".CaseSumTwo STEXT size= args=0x4 locals=0xc
0x0000 (point.go:) TEXT "".CaseSumTwo(SB), $-
...
// point.go:31 -> data := GenerateData()
// point.go:34 -> *result += data[i]
0x001a (point.go:) MOVL (SP), AX
0x0027 (point.go:) MOVL "".result+(SP), BX
0x002f (point.go:) MOVL (AX)(BP*), SI // 栈寄存器移动四个字节, -> SI源变址寄存器
0x0032 (point.go:) ADDL SI, (BX) // SI
0x0034 (point.go:) INCL BP
0x0035 (point.go:) CMPL BP, DX
0x0037 (point.go:) JGE
0x0039 (point.go:) TESTB AX, (BX)
0x003b (point.go:) CMPL BP, CX
0x003d (point.go:) JCS
0x003f (point.go:) JMP
0x0041 (<unknown line number>) PCDATA $, $-
0x0041 (<unknown line number>) PCDATA $, $-
0x0041 (<unknown line number>) ADDL $, SP
0x0044 (<unknown line number>) RET
0x0045 (point.go:) PCDATA $, $
0x0045 (point.go:) PCDATA $, $
0x0045 (point.go:) CALL runtime.panicindex(SB)
0x004a (point.go:) UNDEF
0x004c (point.go:) NOP

函数CaseSumThree

"".CaseSumThree STEXT size= args=0x4 locals=0x10
0x0000 (point.go:) TEXT "".CaseSumThree(SB), $-
...
// point.go:40 -> data := GenerateData()
// point.go:42 -> tmp := *result
// point.go:44 -> tmp += data[i]
// point.go:46 -> *result = tmp
0x001a (point.go:) MOVL (SP), AX
0x0021 (point.go:) PCDATA $, $
0x0021 (point.go:) PCDATA $, $
0x0021 (point.go:) MOVL "".result+(SP), DX
0x0025 (point.go:) MOVL (DX), BX // ->BX数据指针寄存器
0x0027 (point.go:) MOVL "".testData+(SB), BP
0x002d (point.go:) XORL SI, SI
0x002f (point.go:) JMP
0x0031 (point.go:) LEAL (SI), DI
0x0034 (point.go:) MOVL DI, "".i+(SP) // 移动DI到栈指针12字节的位置
0x0038 (point.go:) MOVL (AX)(SI*), DI // 源变址寄存器移动四个字节(32位),-> 目的变址寄存器
0x003b (point.go:) ADDL DI, BX // DI+BX
0x003d (point.go:) MOVL "".i+(SP), DI
0x0041 (point.go:) MOVL DI, SI
0x0043 (point.go:) CMPL SI, BP
0x0045 (point.go:) JGE
0x0047 (point.go:) CMPL SI, CX
0x0049 (point.go:) JCS
0x004b (point.go:) JMP
0x004d (point.go:) PCDATA $, $
0x004d (point.go:) MOVL BX, (DX)
0x004f (point.go:) ADDL $, SP
0x0052 (point.go:) RET
0x0053 (point.go:) CALL runtime.panicindex(SB)
...

比较结论

CaseSumTwo函数,在进行ADDL之前,因为“*result”为指针变量,所以不能直接与data[i]运算。因此需要创建一个栈空间,并指向data的地址并,然后通过移动栈指针后得到下一个值的地址,并赋与SI。
CaseSumThree函数,在进行ADDL执行前,创建了一个值变量,那么在执行ADDL的时候,只需要移动SI获取下一个data的值就可以直接进行算数运算,中间少了地址的引用的栈的操作。

堆和栈

其实说白了,就是CaseSumTwo中 *result内存是分配在堆上的,而 CaseSumThree中 tmp是分配在栈上的,而堆和栈堆性能区别这里做一个简单堆比较:

  1. 有寄存器直接对栈进行访问(esp,ebp),而对堆访问,只能是间接寻址。 也就是说,可以直接从地址取数据放至目标地址;使用堆时,第一步将分配的地址放到寄存器,然后取出这个地址的值,然后放到目标地址。

  2. 栈中数据cpu命中率更高,满足局部性原理。

  3. 栈是编译时系统自动分配空间,而堆是动态分配(运行时分配空间),所以栈的速度快。

  4. 栈是先进后出的队列结构,比堆结构相对简单,分配速度大于堆。

总结

本章主要讲了三个点:

  1. 消除循环的低效率
  2. 减少过程调用
  3. 消除不必要的内存引用

引用《深入计算机系统原理》一书中对性能优化所提到的三个方面:

  1. 高级设计,为遇到的问题选择适当的算法和数据结构。要特别警觉,避免使用那些会渐进地产生糟糕性能的算法或编码技术。
  2. 基本编码原则,从指令的角度考虑,开发中应如何编码,才能减少执行的指令。
  3. 低级优化,针对现代处理器,如何让cpu的流水线尽量饱合。

所以,一个优秀的程序员在写每一行代码,定义每一个变量,也许背后思考的就会更多。

原文地址

https://github.com/WilburXu/blog/blob/master/Golang/Go%20%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E4%B9%8B%E6%A1%88%E4%BE%8B%E4%B8%80.md

Go 性能分析之案例一的更多相关文章

  1. 性能分析(2)- 应用程序 CPU 使用率过高案例

    性能分析小案例系列,可以通过下面链接查看哦 https://www.cnblogs.com/poloyy/category/1814570.html 系统架构背景 其中一台用作 Web 服务器,来模拟 ...

  2. 性能分析(3)- 短时进程导致用户 CPU 使用率过高案例

    性能分析小案例系列,可以通过下面链接查看哦 https://www.cnblogs.com/poloyy/category/1814570.html 系统架构背景 VM1:用作 Web 服务器,来模拟 ...

  3. 性能分析(4)- iowait 使用率过高案例

    性能分析小案例系列,可以通过下面链接查看哦 https://www.cnblogs.com/poloyy/category/1814570.html 前言 前面两个案例讲的都是上下文切换导致的 CPU ...

  4. 性能分析(5)- 软中断导致 CPU 使用率过高的案例

    性能分析小案例系列,可以通过下面链接查看哦 https://www.cnblogs.com/poloyy/category/1814570.html 前言 软中断基本原理,可参考这篇博客:https: ...

  5. 性能分析(7)- 未利用系统缓存导致 I/O 缓慢案例

    性能分析小案例系列,可以通过下面链接查看哦 https://www.cnblogs.com/poloyy/category/1814570.html 前提 前面有学到 Buffer 和 Cache 的 ...

  6. 性能分析(1)- Java 进程导致 CPU 使用率升高,问题怎么定位?

    性能分析小案例系列,可以通过下面链接查看哦 ps:这些分析小案例不能保证百分比正确,是博主学习过程中的总结,仅做参考 前提 本机有一个很占用 CPU 的项目,放在了 Tomcat 下启动着 如何定位 ...

  7. 性能分析(6)- 如何迅速分析出系统 CPU 的瓶颈在哪里

    性能分析小案例系列,可以通过下面链接查看哦 https://www.cnblogs.com/poloyy/category/1814570.html 前言 在做性能测试时,我们会需要对 Linux 系 ...

  8. (转)一个MySQL 5.7 分区表性能下降的案例分析

    一个MySQL 5.7 分区表性能下降的案例分析 原文:http://www.talkwithtrend.com/Article/216803 前言 希望通过本文,使MySQL5.7.18的使用者知晓 ...

  9. [转] Android 性能分析案例

    Android 系统的一个工程师(Romain Guy)针对Falcon Pro  应用,撰写了一个Android性能分析的文章.该文章介绍了如何分析一个应用哪里出现了性能瓶颈,导致该应用使用起来不流 ...

随机推荐

  1. 继400G后,QSFP-DD800G会是下一个风口吗?

    数据中心市场作为光通信企业的主要战场,近三年400G的热度一直都在持续,虽有Facebook F16继续选用100G架构给市场泼了一些冷水等插曲存在,但近日随着阿里巴巴硅光400G QSFP-DD D ...

  2. axios.js 在测试机ios7.1的iphone4中不能发送http请求解决方案

    原因:axios使用promise语法,浏览器不支持该语法 解决思路:使浏览器支持promise语法 具体代码: 安装es6-promise,npm i es6-promise -D. 在引入axio ...

  3. php设计模式;抽象类、抽象方法

    设计模式 什么叫设计模式 所谓设计模式,就是一些解决问题的“常规做法”,是一种认为较好的经验总结.面对不同的问题,可能会有不同的解决办法,此时就可以称为不同的设计模式. 工厂模式 在实际应用中,我们总 ...

  4. MySQL引擎类型(三)

    InnoDB: 1)经常更新的表,适合处理多重并发的更新请求. 2)支持事务. 3)可以从灾难中恢复(通过bin-log日志等). 4)外键约束.只有他支持外键. 5)支持自动增加列属性auto_in ...

  5. C++基础(静态数据成员和静态成员函数)

    [简介] 1.静态数据成员在类中声明,在源文件中定义并初始化: 2.静态成员函数没有this指针,只能访问静态数据成员: 3.调用静态成员函数:(1)对象.(2)直接调用: 4.静态成员函数的地址可用 ...

  6. 15 飞机大战:pygame入门、python基础串连

    0 pygame模块的导入 import pygame导入pygame包 使用pygame.init()导入pygame的所有模块.只有导入模块pygame才能使用. 使用pygame.quit()卸 ...

  7. GitHub的Fork是什么意思

    GitHub的Fork 是什么意思[转] GitHub Help Simple guide to forks in GitHub and Git GitHub的Fork 是什么意思-N神3-博客园 G ...

  8. 怎样获取iframe节点的window对象

    需要使用iframeElement.contentWindow;  var frame = document.getElementById('theFrame'); var frameWindow = ...

  9. 音视频入门-08-RGB&YUV

    * 音视频入门文章目录 * YUV & RGB 相互转换公式 YCbCr 的 Y 与 YUV 中的 Y 含义一致,Cb 和 Cr 与 UV 同样都指色彩,Cb 指蓝色色度,Cr 指红色色度,在 ...

  10. sva 基础语法

    断言assertion被放在verilog设计中,方便在仿真时查看异常情况.当异常出现时,断言会报警.一般在数字电路设计中都要加入断言,断言占整个设计的比例应不少于30%.以下是断言的语法: 1. S ...