大部分的游戏开发过程中,会出现大量的游戏对象,大量的游戏对象产生大量的对象属性,不可避免地会使用到map也就是hash字典来存储数据。

那么为什么需要用字典存储呢?我开发过程中经常遇到的是这样的需求,游戏单位有自己的唯一标识符,任何对象只能通过这个标识符来唯一定位。这种情况下,数组下标就是一个比较尴尬的东西,如果用数组下标作为标识符,那么数据只能自增的情况下数组会无限扩展,显然不可行。如果将数组下标作为附加的标识,那就代表着需要维护两个能定位到对象的参数,逻辑上埋下了一些隐患。所以最后大部分情况下都选择了hash字典来存储。看起来非常美好。随用随存。直接key访问和移除。大部分时候到这里就结束了。

but!随着开发的持续以及字典的大量使用和逻辑参与的情况下。可怕的情况出现了。map的遍历大量出现在逻辑中。我自己的游戏在开发后期通过profiler查看cpu耗时的时候发现。有80%以上的耗时出现在map的遍历上。是单纯的map遍历。而不包括获取对象之后的操作。这就非常可怕了。于是做了许多测试和尝试。发现slice的遍历在数据量非常大的情况下。依然有非常好的体验。于是有了一些大胆的想法。能不能构造一个既包含map特性同时又有slice性能的结构体呢。

首先看一下这段测试代码。

func SliceAndMapTest() {mp := map[int]int{}testSize := 9999999slice := make([]int, testSize)now := time.Now()fmt.Printf("开始测试Map插入\n")for i := 0; i < testSize; i++ {mp[i] = i}fmt.Printf("数据量 %v Map插入耗时 %v\n", testSize, time.Since(now))now = time.Now()fmt.Printf("开始测试Slice插入\n")for i := 0; i < testSize; i++ {slice[i] = i}fmt.Printf("数据量 %v Slice插入耗时 %v\n", testSize, time.Since(now))now = time.Now()fmt.Printf("开始测试Map range遍历\n")for i, j := range mp { //无序的_, _ = i, j}fmt.Printf("数据量 %v Map range遍历耗时 %v\n", testSize, time.Since(now))now = time.Now()fmt.Printf("开始测试Map index遍历\n")for i := 0; i < testSize; i++ {_, _ = mp[i]}fmt.Printf("数据量 %v Map index遍历耗时 %v\n", testSize, time.Since(now))now = time.Now()fmt.Printf("开始测试Slice range遍历\n")for i, j := range slice {_, _ = i, j}fmt.Printf("数据量 %v Slice range遍历耗时 %v\n", testSize, time.Since(now))now = time.Now()fmt.Printf("开始测试Slice index遍历\n")for i := 0; i < testSize; i++ {_ = slice[i]}fmt.Printf("数据量 %v Slice index遍历耗时 %v\n", testSize, time.Since(now))
}

这是执行后的输出。

开始测试Map插入
数据量 9999999 Map插入耗时 1.8680628s
开始测试Slice插入
数据量 9999999 Slice插入耗时 22.478ms
开始测试Map range遍历
数据量 9999999 Map range遍历耗时 129.8605ms
开始测试Map index遍历
数据量 9999999 Map index遍历耗时 692.2344ms
开始测试Slice range遍历
数据量 9999999 Slice range遍历耗时 2.9917ms
开始测试Slice index遍历
数据量 9999999 Slice index遍历耗时 2.9932ms

可以看到slice的性能暴打map。那么如何构造一个既拥有slice特性又有map的无限扩展性的结构呢。我思考了很久。最后想到将两个key,也就是slice的index以及map的key进行结合。再生成唯一key去进行操作的方式。 看看具体的实现。

package funs//用slice代替map做快速访问的结构type SliceMapValidInterface interface {Cache()        //回收时的处理 只需要处理外部的回收逻辑 非必要接口 可以直接为空函数IsValid() bool //判定数据是否依然有效 非必要接口 可以直接返回true
}type SliceMap struct {Index      int64   //当前的可用indexCacheIndex []int64 //当前的回收index列表Vs         []SliceMapValidInterfaceMax        int64 //当前的最大index上限BaseAppend int64 //数据位不够的时候的自动扩展值
}func getSliceIndex(combineIndex int64) int64 {return combineIndex << 32 >> 32 //过滤掉左边32位的数据 剩下的就是真实的数组index
}//其实使用过程中大部分时候combineIndex本身作为外部唯一key就够了 mapIndex只是过度
func GetMapIndex(combineIndex int64) int64 {return combineIndex >> 32
}func getCombineIndex(sliceIndex int64, mapIndex int64) int64 { //将数组index和字典index合并为一个唯一indexreturn mapIndex<<32 ^ (sliceIndex << 32 >> 32)
}func GetNewSliceMap(baseMax int64, baseAppend int64) SliceMap {if baseAppend <= 0 {baseAppend = 1}return SliceMap{Index:      1, //0位数据舍弃 有些讨厌的逻辑要判定0CacheIndex: []int64{},Vs:         make([]SliceMapValidInterface, baseMax),Max:        baseMax,BaseAppend: baseAppend,}
}func (this *SliceMap) Clear(cache bool) {if cache {for i := int64(0); i < this.Index; i++ {buf := this.Vs[i]if buf != nil {buf.Cache()this.Vs[i] = nil}}} else {for i := int64(0); i < this.Index; i++ {buf := this.Vs[i]if buf != nil {this.Vs[i] = nil}}}this.Index = 1 //重置index和回收列表if len(this.CacheIndex) > 0 {this.CacheIndex = []int64{}}
}func (this *SliceMap) Range(f func(SliceMapValidInterface) bool) {for i := int64(0); i < this.Index; i++ {in := this.Vs[i]if in != nil && in.IsValid() {if !f(in) {break}}}
}//单纯移除Index不调用回收函数 游戏开发中移除通常是做一个标记位 等逻辑全部执行完毕在帧末再遍历移除 所以提供了这样临时移除的函数
func (this *SliceMap) JustRemove(index int64) {index = getSliceIndex(index)if this.Vs[index] != nil {this.Vs[index] = nilthis.CacheIndex = append(this.CacheIndex, index)}
}//移除且调用回收函数
func (this *SliceMap) Remove(index int64) bool {index = getSliceIndex(index)if this.Vs[index] != nil {this.Vs[index].Cache()this.Vs[index] = nilthis.CacheIndex = append(this.CacheIndex, index)return true}return false
}//获取指定combineIndex的对象
func (this *SliceMap) Get(combineIndex int64) (SliceMapValidInterface, bool) {index := getSliceIndex(combineIndex)if index >= 0 {v := this.Vs[index]if v != nil && v.IsValid() {return v, true}}return nil, false
}//存入对象并返回唯一的combineIndex作为后续记录的关键
func (this *SliceMap) Save(in SliceMapValidInterface, mapIndex int64) int64 {index := int64(0)if in != nil {if len(this.CacheIndex) > 0 {index = this.CacheIndex[0]this.Vs[index] = inthis.CacheIndex = this.CacheIndex[1:]} else {for {if this.Index >= this.Max {if this.BaseAppend <= 0 {this.BaseAppend = 1}this.Vs = append(this.Vs, make([]SliceMapValidInterface, this.BaseAppend)...)this.Max += this.BaseAppend} else {break}}if this.Index < this.Max {index = this.Indexthis.Vs[index] = inthis.Index++}}return getCombineIndex(index, mapIndex)}return -1
}

这样的话。数据又有无限自增的key(mapIndex)。也有数组内部访问的key(sliceIndex)。也有合并后全局唯一的key(combineIndex)。

那么同样做一下新结构的测试。

func SliceAndSliceMapTest() {testSize := 9999999slice := make([]int, testSize)sliceMap := GetNewSliceMap(int64(testSize), 1)now := time.Now()fmt.Printf("开始测试Slice插入\n")for i := 0; i < testSize; i++ {slice[i] = i}fmt.Printf("数据量 %v Slice插入耗时 %v\n", testSize, time.Since(now))now = time.Now()fmt.Printf("开始测试SliceMap插入\n")for i := 0; i < testSize; i++ {buf := &SliceMapValidSc{}buf.CombineIndex = sliceMap.Save(buf, int64(i))}fmt.Printf("数据量 %v SliceMap 插入耗时 %v\n", testSize, time.Since(now))now = time.Now()fmt.Printf("开始测试Slice range遍历\n")for i, j := range slice {_, _ = i, j}fmt.Printf("数据量 %v Slice range遍历耗时 %v\n", testSize, time.Since(now))now = time.Now()fmt.Printf("开始测试Slice index遍历\n")for i := 0; i < testSize; i++ {_ = slice[i]}fmt.Printf("数据量 %v Slice index遍历耗时 %v\n", testSize, time.Since(now))now = time.Now()fmt.Printf("开始测试 SliceMap 遍历\n")sliceMap.Range(func(in SliceMapValidInterface) bool {_ = (in.(*SliceMapValidSc)).CombineIndexreturn true})fmt.Printf("数据量 %v SliceMap 遍历耗时 %v\n", testSize, time.Since(now))
}

输出如下。

开始测试Slice插入
数据量 9999999 Slice插入耗时 7.9781ms
开始测试SliceMap插入
数据量 9999999 SliceMap 插入耗时 265.6321ms
开始测试Slice range遍历
数据量 9999999 Slice range遍历耗时 2.9924ms
开始测试Slice index遍历
数据量 9999999 Slice index遍历耗时 4.9866ms
开始测试 SliceMap 遍历
数据量 9999999 SliceMap 遍历耗时 39.8935ms

可以看到虽然不如原生slice那样的快。毕竟内部做了很多判定和操作。 但是无论如何是比map的性能优化了非常多。后续也许还有优化点。但是我们的项目在做完这个优化之后瓶颈就不出现在这里了。就没做更多的尝试。

游戏开发中字典数据的优化方案 golang版本相关推荐

  1. 游戏开发中的专业术语

    本文整理了网络/游戏/编程相关的专业术语,作为游戏开发中的辅助参考资料,后期如果遇到其他的术语还会更新. 16毫秒 / 帧速率 Frame Rate. 电子游戏使用的光栅显示器是普通电视时,图像一般每 ...

  2. 【《Real-Time Rendering 3rd》 提炼总结】(十一) 第十四章 : 游戏开发中的渲染加速算法总结

    本文由@浅墨_毛星云 出品,转载请注明出处.   文章链接: http://blog.csdn.net/poem_qianmo/article/details/78884513 导读 这是一篇1万3千 ...

  3. VR硬件演进与其游戏开发中的若干注意事项

    最近两年虚拟现实(Virtual Reality,简称VR)从刚刚走进公众视野到逐渐变得炙手可热,很多不同领域的IT开发者都想进入虚拟现实领域.本篇文章将首先讲解VR入门所需要学习的知识,然后从VR软 ...

  4. 网络同步在游戏历史中的发展变化 — 优化技术总结

    目录(终篇): 六.TCP VS UDP 七.常见同步优化技术 1.表现优化 - 插值优化 - 客户端预先执行+回滚 2.延迟对抗 - 延迟补偿 - 命令缓冲区 - 通过具体的实现技巧 3.丢包对抗 ...

  5. 【转载】【《Real-Time Rendering 3rd》 提炼总结】(十一) 第十四章 : 游戏开发中的渲染加速算法总结

    本文由@浅墨_毛星云 出品,转载请注明出处.    文章链接:  http://blog.csdn.net/poem_qianmo/article/details/78884513 导读 这是一篇1万 ...

  6. 游戏开发中的数据表示

    声明:本文内容源自腾讯游戏学院程序公开课_服务端 一.数据表示的基础 什么是数据表示? 数据是信息的载体. 数据表示是一组操作,可以描述.显示.操作信息. 数据表示的要素 IDL - 接口描述语言 I ...

  7. 软件系统开发中的数据交换协议

    在很多地方都有"数据交换"这个概念,本文所说的"数据交换" 是指在计算机网络中,一个系统把数据传递给另外一个系统.这非常类似于一个人要告诉另外一个人一件事情. ...

  8. 游戏开发中常用的数据结构和算法

    转载Loving_初衷 前言 时间流逝,物是人非,就好像涌动的河流,永无终焉,幼稚的心智将变得高尚,青年的爱慕将变得深刻,清澈之水折射着成长. ----------<塞尔塔传说> PS:为 ...

  9. 游戏开发中的人工智能(五):以势函数实现移动

    接上文: 游戏开发中的人工智能(四):群聚 本文内容:靠势能移动在游戏 AI 程序中还算相当新颖.这个方法的最优越的地方在于可以同时处理追逐.闪躲.成群结队和避免碰撞等行为.我们专门研究的这个势函数叫 ...

最新文章

  1. Android10剪贴板,剪纸堆 Clip Stack - 轻量级剪贴板管理程序(支持 Android 10)
  2. 孔兵 库卡机器人_名企零距离 专访库卡首席执行官 孔兵先生
  3. Windows 技术篇 - win10复制文件或文件夹时出错,提示“文件或目录损坏且无法读取“问题解决。windows驱动器、磁盘修复方法
  4. Oracle-AWR管理包DBMS_WORKLOAD_REPOSITORY.MODIFY_SNAPSHOT_SETTINGS
  5. 小样本学习 | Learning to Compare: Relation Network for Few-Shot Learning
  6. 每周一题 —— 3n+1问题
  7. 计算机网络技术超文本,网络协议确定了计算机网络传递和管理信息的规范,其中HTTP属于()A、超文本传输协议B、传输控制协...
  8. DBA_Oracle Table Partition表分区概念汇总(概念)
  9. number输入框限制输入数字位数、字体随数字长度变化
  10. Linux中如何让进程(或正在运行的程序)到后台运行?[zz]
  11. Model 3车主对FSD套件不满意 德国法院下令特斯拉回购汽车
  12. linux的挂载的问题,重启后就挂载就没有了
  13. Cover Protocol发起新提案,为Nexus Mutual提供保险覆盖
  14. JavaScript循环结构(1)
  15. cad户型图练习_我的房子我做主 篇一:技多不压身—业内人士手把手教你学会用CAD绘制户型图...
  16. 计算机函数公式相乘,excel表格数据相乘公式-如何在Excel中使用乘法函数公式
  17. 我,90后,从审计员到程序员,四年在南京买房
  18. 一个人的职业生涯之旅 —— 应届生求职、面试、Offer、跳槽(发展瓶颈、薪资倒挂、职业倦怠、骑驴找马、简历优化)问题分享
  19. ADS学习:统计分析——蒙特卡洛分析、良率分析
  20. 第二课:为什么要教授财务知识

热门文章

  1. 有哪些好用的读书笔记app
  2. 数据库+jdbc实现学生教师管理
  3. Centos7安装HighGo DB V6企业版
  4. 拓尔思信息科技股份有限公司2019校园春季招聘
  5. Win10 安装编译器|调试器 TDM-GCC/Mingw64
  6. 电商网站秒杀系统如何设计
  7. 如何安装正版的Xmanager
  8. HTML元素分类:inline、inline-block、block
  9. ffmpeg 多视频 画中画
  10. python requests中content与text方法的区别