您现在的位置是:首页 > 文章详情

什么?GO竟然比PHP慢2倍!

日期:2020-12-04点击:537

这是一篇优化go代码的文章,起这个题目的原因是,我觉得看到标题你们肯定啪的一下就点进来很快呀,但无论你是谁,我都是你的友军,我是一名PHPer,同时我不认为go会比php慢2倍。

这是知名网友E同学提出来的代码,因为获取时间戳、字符拼接、字典使用更贴合实际开发,所以拿来比较有意义。

很引战,不过码林还是要以和为贵,用代码说话。

咱们先看下php、golang版本,运行时间和代码。<!--more-->

版本

➜ hash php -v PHP 7.3.11 (cli) (built: Apr 17 2020 19:14:14) ( NTS ) Copyright (c) 1997-2018 The PHP Group Zend Engine v3.3.11, Copyright (c) 1998-2018 Zend Technologies 
➜ hash go version go version go1.13.14 darwin/amd64 

执行时间

➜ hash php hash.php 352.29206085205ms ➜ hash go run hash.go -args 0 f V1 664.231858ms 

php代码

<?php $startTime = microtime(true); $arr = array(); for($i=0;$i<1000000;$i++){ $currentTime = time(); $key = $i . "_" .$currentTime; $arr[$key] = $currentTime; } $endTime = microtime(true); echo ($endTime - $startTime) * 1000 . "ms\r\n"; 

go第一个版本代码

package main import ( "fmt" "os" "reflect" "runtime/pprof" "runtime/trace" "strconv" "time" ) type CalcCollection struct { } func (c *CalcCollection) V0() { arr := map[string]int64{} for i := int64(0); i < 1000000; i++ { value := time.Now().Unix() key := strconv.FormatInt(i, 10) + "_" + strconv.FormatInt(value, 10) arr[key] = value } } // 以下代码请忽略, 作用如下: // 根据参数决定调用CalcCollection.V{ver} // 根据参数决定是否记录trace、profile func main() { ver := os.Args[len(os.Args)-2] isRecord := os.Args[len(os.Args)-1] == "t" calcReflect := reflect.ValueOf(&CalcCollection{}) methodName := "V"+ver m := calcReflect.MethodByName(methodName) if isRecord { traceFile, err := os.Create(methodName + "_trace.out") if err != nil { panic(err.Error()) } err = trace.Start(traceFile) if err != nil { panic("start trace fail :"+ err.Error()) } defer trace.Stop() cpuFile, err := os.Create(methodName + "_cpu.out") if err != nil { panic(err.Error()) } defer cpuFile.Close() err = pprof.StartCPUProfile(cpuFile) if err != nil { panic("StartCPUProfile fail :"+ err.Error()) } defer pprof.StopCPUProfile() memFile, err := os.Create(methodName + "_mem.out") if err != nil { panic(err.Error()) } defer pprof.WriteHeapProfile(memFile) } t := time.Now() m.Call(make([]reflect.Value , 0)) fmt.Println(methodName, time.Now().Sub(t)) } 

来骗、来偷袭

看完代码很go同学肯定很不服气,一眼就看出问题,这位E同学竟然来骗、来偷袭!并且心里已经有很多改写go代码的方案。

我也一样!

接下来我们就采用图文并茂的形式一步一步将go的真实实力释放出来。

分析原始代码

我们先执行命令go run hash.go -args 0 t,这样可以获得三份文件

  • V0_cpu.out : cpu分析文件
  • V0_trace.out : 运行时事件分析文件
  • V0_mem.out : 内存分析文件

我们分别分析下这几个文件,:

首先是cpu文件 go tool pprof -http :9091 V0_cpu.out ,切到火焰图可以看到数字转字符串、map扩容、字符串拼接、gc耗时比较长

"v0版本cpu火焰图"

其次是内存文件 go tool pprof -http :9092 V0_mem.out, 选择alloc_objects,切到source视图,可以看到申请分配的对象比较多,以及分配位置:

"v0版本mem源代码"

最后我们看下trace文件:

总体trace图,可以看到head分配的对象比较多,main所在的goroutine时不时被调度到其它p上去: "v0版本trace图"

main所在的goroutine,gc pause比较高"v0版本main信息图"

main所在的gorutine,存在多处空白没有执行的时间片: "v0版本main trace图"

由于存在分配对象过多的情况,我们顺便看下逃逸分析:

从下图可以看到,字符串拼接的位置发生了逃逸"v0逃逸分析"

所以,我们第一个修改版本会从以下几个方面去提升性能:

  • map初始化的时候指定容量,去除grow带来的耗时
  • strconv.FormatInt 改为 strconv.AppendInt,减少对象分配

v1版本

基于上一节,我们新增代码如下:

func (c *CalcCollection) V1() { nums := int64(1000000) arr := make(map[string]int64,nums) // key,放循环外,可以重复使用 key := make([]byte,0) for i := int64(0); i < nums; i++ { key = key[:0] value := time.Now().Unix() // 改用appendInt,去掉strconv内部[]byte转string的开销 key = strconv.AppendInt(key,i,10) key = append(key,'_') key = strconv.AppendInt(key, value, 10) keyStr := string(key) arr[keyStr] = value } } 

好,我们再执行下代码 go run hash.go 1 f,时间已经和php一致了:

➜ hash go run hash.go 1 f V1 352.412808ms 

不过我们仍然要看下,时间花在哪里:

"v1版本火焰图"

大部分时间都花在内存span申请,以及slice转string了。所以我们第二版本主要做以下两个方面的优化:

  • 统一申请key的内存
  • 直接将[]byte转为string

v2版本

基于上一节,我们新增代码如下:

func (c *CalcCollection) V2() { nums := int64(1000000) arr := make(map[string]int64, nums) // 计算key长度,申请存下所有key的[]byte keyLen := int64(len(strconv.FormatInt(nums, 10)) + 1 + 10) totalLen := keyLen * nums key := make([]byte, totalLen) for i := int64(0); i < nums; i++ { value := time.Now().Unix() // 计算当前循环key的位置 pos := i * keyLen b := key[pos:pos] b = strconv.AppendInt(b, i, 10) b = append(b, '_') b = strconv.AppendInt(b, value, 10) // 直接将[]byte转为string arr[*(*string)(unsafe.Pointer(&b))] = value } } 

我们再执行下代码 go run hash.go 2 f,时间明显优于php了:

➜ hash go run hash.go -args 2 f V2 320.727972ms 

这个时候我们看下火焰图: "v2版本火焰图"

时间基本都在strconv.AppendInt上了,接下去的方向,就是采用更合适的数据结构来存数据、自己实现优化版的int转string方法,不过鉴于优化到目前,性能上已经可以接受了,我就不继续了。

优化总结

我们通过分析go代码的cpu、heap、trace文件后,采用了以下优化手段:

  • 指定map容量,避免频繁grow
  • 使用strconv.AppendInt代替strconv.FormatInt,因为strconv.FormatInt有一次[]byte转string的行为
  • 对于频繁申请的内存,统一申请一块大的内存,减少span分配
  • 对于[]byte转string,可以通过unsafe.Pointer来替代类型转化,减少内存copy

亲爱的小伙伴,你是否还有其他优化的方案,可以打在公屏上一起交流。

语言比较

我们看到php通过写时复制等优化,降低了开发的复杂度,让开发可以更专注业务的开发,一个array走天下,go开发需要了解go的内存模型,相比php要更为复杂,开发杂音会比较多。

同时,go的性能优化上限高于php,而且这个例子其实是极端的,实际业务开发性能的冲突点大部分都是在于io、池、缓存上,goroutine、pool很好的满足了这部分的性能需求,给关注性能的业务提供了一个高性价比的解决方案。

所以,php和go都是世界上最好的语言。

原文地址

原文链接:https://my.oschina.net/xiayongsheng/blog/4775399
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章