Golang 性能提高技术----基础编码原则

前言

  1. 高级设计。为遇到的问题选择适当的算法和数据结构。要特别警觉,避免使用那些会渐进地产生糟糕性能的算法或编码技术

2)基本编码原则。避免限制优化的因素,这样编译器就能产生高效的代码。

  • 消除连续的函数调用。在可能时,将计算移到外循环中。考虑有选择地妥协程序的模块性以获得更大的效率。
  • 消除不必要的存储器引用。引入临时变量来保存中间结果。只有在最后的值计算出来时,才将结果存放在到数组或全局变量中。

3)低级优化

  • 展开循环,降低开销,并且使得进一步的优化成为可能。
  • 通过使用例如多个累积变量和重新结合等技术,找到方法提高指令级并行。
  • 用功能的风格重写条件操作,使得编译采用条件数据传送。

这段话摘自《深入计算机系统原理》一书中,讲的是三3种性能优化方案,针对程序不同层次来提升性能。

高级设计:指的是程序整体的设计,采用适当的算法和数据结构。
基本编码原则:从指令的角度考虑,开发中应如何编码,才能减少执行的指令。
低级优化:针对现代处理器,如何让cpu的流水线尽量饱合。

本文主要只对基本编码原则进行说明,通过一个例子说明其编码的原因,由浅入深。给出的例子是用golang代码编码,虽然《深入理解计算机系统》一书中是用C语言来讲解,但对于也是转成化成机器码的golang来说都是样不多。编码器在转化成机器码的过程中能帮助开发者是有限的,性能的提升更多是需要依赖程序员的设计。

分析

先看一下我们给出的例子,代码如下:

//测试数据
var testData = CreateTestData()


func GetValue(index int) int {
   return testData[index]
}

//获取测试数据长度
func GetDataLen() int{
   return len(testData)
}

//创建一个6000000000大小的整型切片
func CreateTestData()[]int  {
   data := make([]int,6000000000)
   for index,_ := range data{
      data[index] = index % 128
   }
   return data
}

此处我们建立一个比较大的切片数组来做求和运算,之所以选用比较大的测试数据,是为了减少运行中其它因素的干扰影响,达到更明显的对比效果。下面是最开始的求和代码:

func toSum1(result *int)  {
    for i:=0;i< GetDataLen();i++{
        *result += GetValue(i)
    }
}

此求和是将整个数组的所有元素加到result指针上,代码很简洁,但是就是这样的代码有着非常大的优化空间。我们先按从"基本编码原则"里的第一个点分析。

1.消除连续的函数调用

首先是GetDataLen()这句代码并不需要被反复调用,但它现在放在for循环当中。由于调用一个函数,处理器执行会增加一定的延迟,这中间需要过程压栈,更改程序计数器,再加上内部调用len方法也需要一定的消耗。只是调用几次性能上并不会有大的损失,但是成千上百万次的话,性能的差异就明显了。因此以下是对toSum1()函数的改进:

func toSum2(result *int)  {
    dataLength := GetDataLen()
    for i:=0;i< dataLength;i++{
        *result += GetValue(i)
    }
}

toSum2函数,内部定义一个局部变量dataLength来保存长度,从而减少了GetDataLen()的调用。接着通过以下性能测试,来对两次改进进行性能对比,来查看其性能变化。

func BenchmarkData1(b *testing.B)  {
    var sum *int = new(int)
    *sum = 0
    toSum1(sum)
}

func BenchmarkData2(b *testing.B)  {
    var sum *int = new(int)
    *sum = 0
    toSum2(sum)
}

执行查看其结果

goos: darwin
goarch: amd64
pkg: GoTest/power
BenchmarkData1-4           1    161089507196 ns/op
PASS


goos: darwin
goarch: amd64
pkg: GoTest/power
BenchmarkData2-4           1    143682539481 ns/op
PASS

改进后快了将近20s。这里需要注意的是不同的设备测试结果是不一样的,此数据仅供参考,仅证明该改进是有明显的性能提升的。

其次是第二个方法 GetValue(i)的调用,凡是函数或方法的调用就会有额外的损耗(压栈、修改计数器等),虽然对处理器来说这个没算什么,毕竟cpu是以ns为执行单位,但还是对该函数再进一步改造。

//获取测试数据数组
func GetData() []int {
    return testData
}

func toSum3(result *int)  {
    data := GetData()
    dataLength := len(data)
    for i:=0;i< dataLength;i++{
        *result += data[i]
    }
}

新增加了一个函数GetData()用于获取整个切片,不再调用GetValue()。再执行同样的测试得到以下的数据

goos: darwin
goarch: amd64
pkg: GoTest/power
BenchmarkData3-4           1    141323677877 ns/op
PASS

对比toSum2()的测试数据,只是提升了几秒的效率,原因是这次减少的是函数的调用过程,而对切片内容的访问还是需要的,所以看到的效果不是很明显。(注:此次的改动虽然提高了性能,但考虑到如果开发者不希望知道内部数据结构,那该改动影响对该数据内容的抽象。)

2.消除不必要的存储器引用

*result += data[i] 这段代码我们首先要明白处理器指行它的时候要经过什么步骤。它主要的过程需要以下几步:
​ 1.通过result地址值,从内存取出数据放在寄存器中(假设寄存器A)
​ 2.再通过切片数组的首地址获取第i个元素到寄存器中(假设寄存器B)
​ 3.接着将寄存器B 加到寄存器 A中
​ 4.最后再将寄存A写回 result 指向的内存地址

由于*result在这过程中需要反复地读写,是没有必要的操作,因此我们将再对它做以下的改动。

func toSum4(result *int)  {
    k := *result
    data := GetData()
    dataLength := len(data)
    for i:=0;i< dataLength;i++{
        k += data[i]
    }
    *result = k
}

此处的执行将原先放置在 result 位置的复制到局部变量k中,到最后再将k的值写回result位置中。此处相当是将*result保到固定的寄存器中,让其一直被用作求和运算。此处改动,就相当于节省原有4个步聚里面的1、4步聚,变成以下两个:
​ 1.通过切片数组的首地址获取第i个元素到寄存器中(假设寄存器B)
​ 2.接着将寄存器B 加到寄存器 A中(假设 k 指向的是寄存器A)

再看看其性能对比:

goos: darwin
goarch: amd64
pkg: GoTest/power
BenchmarkData4-4           1    131440327341 ns/op
PASS

对比 toSum3,又减少了10秒,效果很明显。虽然这里只是用指针做例子,但数组和结构体也是一样的,对内部变量的访问是同指针类似。(注:结构体是通过首地址计算再去内存中获取对应的变量值)

总结

此处给出的性能提升只是从指令的角度考虑,并在这过程演示了《深入理解计算机系统》书中所讲的基本编码原则所带来的效益。而如果想要对该函数有更大的提升空间,我们还可以从"低级忧化"忧化入手,在本文中就暂不讲解,后续有时间再对其做补充。废话也不多说,为本文总结一句:消除连续的函数调用和不必要的存储器引用

补充

Golang 性能提高技术----低级优化

测试代码

https://github.com/wpnine/PerformanceExample

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,093评论 1 32
  • 概要 64学时 3.5学分 章节安排 电子商务网站概况 HTML5+CSS3 JavaScript Node 电子...
    阿啊阿吖丁阅读 9,146评论 0 3
  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,219评论 0 4
  • 1.安装 https://studygolang.com/dl 2.使用vscode编辑器安装go插件 3.go语...
    go含羞草阅读 1,546评论 0 6
  • 原文地址:C语言函数调用栈(一)C语言函数调用栈(二) 0 引言 程序的执行过程可看作连续的函数调用。当一个函数执...
    小猪啊呜阅读 4,598评论 1 19