什么?GO竟然比PHP慢2倍!
这是一篇优化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耗时比较长:
其次是内存文件 go tool pprof -http :9092 V0_mem.out
, 选择alloc_objects,切到source视图,可以看到申请分配的对象比较多,以及分配位置:
最后我们看下trace文件:
总体trace图,可以看到head分配的对象比较多,main所在的goroutine时不时被调度到其它p上去:
main所在的goroutine,gc pause比较高:
main所在的gorutine,存在多处空白没有执行的时间片:
由于存在分配对象过多的情况,我们顺便看下逃逸分析:
从下图可以看到,字符串拼接的位置发生了逃逸:
所以,我们第一个修改版本会从以下几个方面去提升性能:
- 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
不过我们仍然要看下,时间花在哪里:
大部分时间都花在内存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
这个时候我们看下火焰图:
时间基本都在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都是世界上最好的语言。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
JVM GC耗时频频升高,这次排查完想说:还有谁?
点击上方蓝色字关注我们~ 1. 背景 多个业务线的应用出现LongGC告警 最近一段时间,经常收到CAT报出来的Long GC告警(配置为大于3秒的为Longgc)。 2. 知识回顾 2.1 JVM堆内存划分 新生代(Young Generation) 新生代内被划分为三个区:Eden,from survivor,to survivor。大多数对象在新生代被创建。Minor GC针对的是新生代的垃圾回收。 老年代(Old Generation) 在新生代中经历了几次Minor GC仍然存活的对象,就会被放到老年代。Major GC针对的是老年代的垃圾回收。本文重点分析的CMS就是一种针对老年代的垃圾回收算法。另外Full GC是针对整堆(包括新生代和老年代)做垃圾回收的。 永久代(Perm) 主要存放已被虚拟机加载的类信息,常量,静态变量等数据。该区域对垃圾回收的影响不大,本文不会过多涉及。 2.2 CMS垃圾回收的6个重要阶段 1、initial-mark 初始标记(CMS的第一个STW阶段),标记GC Root直接引用的对象,GC Root直接引用的对象不多,所以很快。 2、con...
- 下一篇
Spring AOP核心类解析,这是最全的一篇了!!
点击上方蓝色“冰河技术”,关注并选择“设为星标” 持之以恒,贵在坚持,每天进步一点点! 作者个人研发的在高并发场景下,提供的简单、稳定、可扩展的延迟消息队列框架,具有精准的定时任务和延迟队列处理功能。自开源半年多以来,已成功为十几家中小型企业提供了精准定时调度方案,经受住了生产环境的考验。为使更多童鞋受益,现给出开源框架地址: https://github.com/sunshinelyz/mykit-delay PS: 欢迎各位Star源码,也可以pr你牛逼哄哄的代码。 写在前面 最近,不少小伙伴在催更【Spring注解驱动开发】专题,好吧,【Spring注解驱动开发】专题确实有很长时间没更新了。那我们从今天开始更新【Spring注解驱动开发】专题,同样的,我们还是以源码解析为主。 文章已同步收录到:https://github.com/sunshinelyz/technology-binghe 和 https://gitee.com/binghe001/technology-binghe 。如果文件对你有点帮助,别忘记给个Star哦! 关注【冰河技术】微信公众号,回复“Sprin...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Docker安装Oracle12C,快速搭建Oracle学习环境
- SpringBoot2全家桶,快速入门学习开发网站教程
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS关闭SELinux安全模块
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- CentOS6,CentOS7官方镜像安装Oracle11G
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Hadoop3单机部署,实现最简伪集群