60

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

 4 years ago
source link: https://www.tuicool.com/articles/bIVniaQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

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

总结

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

测试代码

https://github.com/wpnine/PerformanceExample


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK