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

前言

自上篇文章写了 基础编码原则(https://www.jianshu.com/p/0dafe1059fdc
),已经过了一段时间了,此处是对上篇文章中提到的“低级优化”做个说明。
“低级优化”这个名词的含义是针对现代处理器的结构体系来设计代码,使自己运行的程序更充分发挥出处理器应该有的性能。

了解现代处理器

要明白如何进行“低级优化”,首先要知道现代处理器是如何执行一条指令,又在执行指令中会遇到什么情况会导致性能下降?

在单个处理器的设计中,cpu架构师为了提升指令的吞吐量,在处理器中实现了流水线系统。即原本一条指令的执行,需要经过取指(IF)、译码(ID)、执行(EX)、访存(MEM)、写回(WB)等一系列阶段。如执行一条指令需要等待上一条指令执行,那这个等待是对处理器来讲是一种浪费的行为。于是将一条执令的执行过程划分为许多的周期,在同一个周期内,多条指令能处于不同阶段执行,达到“指令级并行”现象,比如下图:指令i做完了取指阶段,到周期2时,则会进入到了译码阶段,同时会立即让下一条指令i+1进入取指阶段,这样指令i和i+1在周期2并行执行。

本图来源:https://www.cnblogs.com/CorePower/p/CorePower.html

在现代处理器为了使每个周期延迟尽可能小,会一将指令的执行划分成非常简单的步骤,一般采用了很深的流水线 (15或更多的阶段)。虽然采用了流水线的执行,但在实际程序执行中,基本不太可能达到处理流水线百分之百饱合。

例如:指令B需要等待指令A在将计算结果写入寄存a1中,指令B才能获取寄存器a1中的数据做执行计算,在指令B等待的过程是会有几个周期的延迟。又或者遇到条件语句(if),如果处理器预测进入流水线执行的指令不对,则需要取消后面加载的指令,重新载入新的指令,这样也会白白流费十几个周期。

正常编写出的程序很难让流水线达到饱和,但我们可以想办法使流水线尽量饱和。以下是通过举一个例子来说明程序如何通过此方式来提高性能,希望能帮助大家理解。

演示代码

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

此段代码是上篇文章中已经是经过指令级优过后的代码,现在我们再对它进行"低级优化"

测试数据

此次的测试代码同上次的代码是一样的,由于上次写的文章同此次的测试环境也有所差异,因此对上次的代码还得重新测试

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

toSum4()新的测试结果如下:

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

循环展开

循环展开是通过程序的变换,通过增加每次迭代计算的元素,减少循环的迭代次数。对原有tosum4代码做以下的变换

func toSum5(result *int)  {
    k := *result
    data := GetData()
    dataLength := len(data)
    for i:=1;i< dataLength;i+=2{
        k += data[i] + data[i - 1]
    }
    if dataLength % 2 == 1{
        k += data[dataLength-1]
    }

    *result = k
}

将 k += data[i] 变成 k += data[i] + data[i-1],这样做会带来怎么样的好处呢?

for语句中,每次迭代中都需要进行条件判断(i< dataLength),减少迭代次数,意味着可以节省掉一半的条件判断指令执行(i<datalength),其次每次迭代可以减少几个周期的延迟,因为在后续的指令执行需要等待控制语句更新程序计数器才能往下继续执行,在计数器没有更新时,是不能将计算结果更新到寄存器或内存中的。

提高并行性

虽然我们在上面进行展开了代码,但是还是不能让流水线达到饱合,考虑到 k += data[i] + data[i - 1],此处实际运行中可能是先执行k+=data[i],等计算结果写入到寄存器中才能执行 k+=data[i-1]。对于加法指令可能并不会有太多的周期延迟,但是如果是针对乘除指令就会比较明显。因此,我们可以用多个累积变量来打破顺序等待的过程,让着两步操作'并行'执行

对 toSume5()函数改动后变成如下:

func toSum6(result *int)  {
    k1 := 0
    k2 := 0
    data := GetData()
    dataLength := len(data)

    for i:=1;i< dataLength;i+=2{
        k1 += data[i]
        k2 += data[i - 1]
    }

    //如果是传入的数量是奇数,则单独对最后一个数进行累加
    if dataLength % 2 == 1{
        k1 += data[dataLength-1]
    }

    *result = k1 + k2
}


此处改动将原有一个累积变量 k 变成 两个,分别是k1和k2,两个累积量将 k1 += data[i] 和 k2 = data[i-1]之间的指令变成不会相互依赖,指令k2 = data[i-1]不需要等待上条指令的结果才能往下执行。

测试结果

我们对toSum6()进行性能测试,得到以下数据

goos: darwin
goarch: amd64
pkg: GoTest/power
BenchmarkData6-4           1    150068753563 ns/op
PASS

对比toSum4() 和 toSum6()的性能对比又有了明显10s的提升。在这里只是对程序进行2次展开2次并行的处理,如果想让流水线更多饱合,那还可以进行更多的展开和并行处理,当然不是越多越好,考虑到寄存器的有限,如果累加值超过剩余寄存器的数量,增加多余的内存读写操作反而得不偿失,具体展开和并行次数还得根据程序所在的机器运行的情况决定。

总结

本文参考自《深入理解计算机系统》,在此只是举了一个例子,通过展开和并行的优化来加强对流水线的利用,表现出"低级优化"带来的效果。虽然正常的开发中我们很少会这样处理,同时对代码这样改动可能会变得更难理解。但是如果针对频繁运行的核心代码来优化,那这样的优化是非常有必要的。某些大神的开源代码,他们在编码的考虑非常多,希望下次看到类似这种展开和多累积量的代码,能帮助大家更明白其用意。

代码

https://github.com/wpnine/PerformanceExample

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

推荐阅读更多精彩内容