Go语言如何在不牺牲运行时性能的情况下提高表达性

14 篇文章 0 订阅
1 篇文章 0 订阅

今天看了一篇很有趣的博文,讲使用Go写代码时。对于更新数组,一个小小的改动却能大大提高代码的效率。而自己平时最经常使用的居然是效率最低的一种方式。不得不感慨Go虽然入门容易,但是像C一样也是博大精深,细节无处不在,平时需要注意的地方,需要深挖的地方太多太多。这里把文章翻译撸一下给大家分享下,文章很短,可能只需要你马桶上5分钟的时间,你就将Get一个很牛逼的技能。

只撸具体流程,需要看详细原文的,请移步:

原文地址:How the Go language improves expressiveness without sacrificing runtime performance

前提

加入你有一个数据结构,用于一系列的计数,数据结构如下:

type E struct {
        A, B, C, D int
}

var e = make([]E, 1000)

然后你需要一个方法来更新数组e中的计数。

最常用的方法

这个问题so easy,大家一拍脑袋就能想到的就是:无非就是一个循环便利数组,然后更新就可以了。然后有了下面的方法(为了大家能够快速理解,将原文代码稍有更改,后面示例同):

func BenchmarkManual(b *testing.B) {
        var e = make([]E, SIZE)
        for j := 0; j < b.N; j++ {
                for i := range e {
                        e[i].A += 1
                        e[i].B += 2
                        e[i].C += 3
                        e[i].D += 4
                }
        }
}

这种方法有个问题:大家知道面向对象编程中为了让码农能将更多的精力集中在业务上,对数据类型进行了完美的封装。类似的,对应到这里就是你在访问一个数组元素时,不必考虑数组索引是否越界,编译器这里会自动在每次访问e[i]之前塞一个边界检查进去(这里并不是面向对象式的在类中封装了边界检查,而是有编译器承担了相应的工作,但是思想类似。)。所以问题来了,这里每次循环里面更新一个e[i]的元素时,需要访问四次e[i]就有4次边界检查,有3次都是多余的。

改进

那么已经知道问题在哪里了,优化就很好做了,只用干掉那三次多余隐藏的边界检查就可以了。

func BenchmarkUnroll(b *testing.B) {
        var e = make([]E, SIZE)
        for j := 0; j < b.N; j++ {
                for i := range e {
                        v := &e[i]
                        v.A += 1
                        v.B += 2
                        v.C += 3
                        v.D += 4
                }
        }
}

这种方法取了e[i]的地址,然后再做更新操作。所以只用做取地址那一次的边界检查即可。但是这种写法总有点怪怪的,别人看到可能要绕很大一个圈想v := &e[i]这一步到底是啥意思,完全多此一举嘛。可能更有甚者还会纠结,为什么是取地址,然后直接操作地址,而不是操作地址的值呢…巴拉巴拉,总之怪怪的啦。

再改

好了,既然E是一个结构体,我们给他封装一个方法试试:

func (e *E) update(a, b, c, d int) {
        e.A += a
        e.B += b
        e.C += c
        e.D += d
}

func BenchmarkUpdate(b *testing.B) {
        var e = make([]E, SIZE)
        for j := 0; j < b.N; j++ {
                for i := range e {
                        e[i].update(1, 2, 3, 4)
                }
        }
}

这种方法因为update声明的是属于(e *E),所以在执行e[i].update(1,2,3,4)时,会自动塞如e[i]的地址&e[i]。同样只会做一次边界检查,所以这种方法和第二种有异曲同工之妙。只是因为封装了一层,函数调用涉及到栈的处理,效率不如第二种方法。

结果

执行benchmark测试,三种方法结果如下。可以看到第一种方法确实是性能最差的。第二种方法最好,第三种因为涉及到栈相关的操作,性能稍差。但是综合权衡代码可读性、可维护性和性能,第三种无疑是最好的。
还有貌似函数调用的压栈、出栈在这里性能上的损失很小,博主觉得从获得更好的代码可读性、可维护性这点损失完全可以忽略。

BenchmarkManual-4     300000          4714 ns/op
BenchmarkUnroll-4    1000000          1725 ns/op
BenchmarkUpdate-4    1000000          1884 ns/op
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值