前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >黑科技:用UE4的FName优化掉100MB的Lua内存

黑科技:用UE4的FName优化掉100MB的Lua内存

作者头像
quabqi
发布2021-11-04 10:51:56
2K0
发布2021-11-04 10:51:56
举报
文章被收录于专栏:Dissecting UnrealDissecting Unreal

FName

FName是UE4提供的一种特殊的字符串类型。FName和FString不一样的地方是,他的对象内部并不直接存储字符串,而是把字符串存储在一个全局的NamePool之中,而FName的内部存储着字符串在NamePool中的索引。他的容量非常小,当游戏逻辑在用来传递参数,比较等操作时,只传递或比较索引,而不需要对字符串本身的内容做操作,就可以显著的提升游戏性能。如果你的游戏也用到了Lua并且清楚Lua的字符串内部细节,在看到了这样简短的FName介绍和这个唬人的标题后,相信你这是一定已经有了想法,我会在后面介绍Lua的改造细节。可以略过前面FName部分,直接跳到后面看。

FName的成员变量

FNameEntryId的结构

第一张图可以看到FName本身只有3个变量,而其中一个只在定义了宏WITH_CASE_PRESERVING_NAME的情况下有效(引擎默认是在编辑器中会开启,游戏环境中会关闭),其中FNameEntrtyId在第二张图中可以看到内部只是一个uint32,因此FName本质上的成员变量就只有3个uint32变量12字节,在不开启区分FName大小写的环境中只有8字节,相当于一个指针的大小。

其中,ComparisonIndex是当前字符串在全局NamePool的索引,而Number是字符串的数字部分。有了全局的NamePool,当创建相同的FName时,只要让他们的ComparisonIndex相同就可以共用内存,起到节省内存的目的,因为UE4内部UObject习惯用字符串+数字来存储对象的名字,将同样的字符串合并存储,而不同的数字放在单独的变量里,又能节省掉大量的内存。

当需要访问FName其中内部内容时,可以使用ToString函数来将字符串转成FString,从而获取到实际的字符串。如上图所示,这个函数的内部就是直接用Index到NamePool中获取,如果有数字后缀,就拼接上最后的"_"+数字。

可以看到FName有很多构造函数可以方便用户去创建,包括直接用已经有的Index创建,用字符串来创建等。其中有个参数FindType会填充默认值FNAME_Add。当使用Add时,内部会把传入的字符串调用Store存入NamePool中,而使用Find就只会查找,在没有的情况下不会新增,如下图所示。

这里需要注意的几个细节:

  1. FName传入的字符串,无论是宽字符还是普通的字符,会统一按照ANSICHAR来存储,因此内部内存一定是最小的版本,无需担心把宽字符存入了FName浪费内存
  2. FName默认在游戏中不区分大小写,但使用ToString时得到的字符串本身是有大小写的,这时字符串的内容是第一次存入的内容,因此要避免业务逻辑使用大小写敏感的代码。如果有多处代码同时存入不同大小写的FName时,这里一定要特别注意
  3. 字符串计算Hash时,使用的是CityHash函数,和FString的GetTypeHash用的函数不同,得到的Hash值也是不同的。另外CityHash在计算小于64字节的Buffer时速度非常快,而大于64字节时会稍微慢一些,因此尽量在FName中存短一些的字符串。
  4. FName的存储之后就不会再释放,因此不要存大量不会用到的字符串。

Lua中的字符串

lua中分为普通的值和gc对象,而字符串就是一种gc对象,如下图所示:

字符串对象在内存上保存的实际是一个字符串头+实际的字符串内容(上图的contents)。字符串头中保存了字符串的Hash,长度等信息。

普通的变量在lua内部结构如上图所示,由Value+类型组成,其中Value是一个union共用体,当不是gc对象时,Value内部就直接存值,而如果是gc对象,Value会存储对象的指针(和UE4的UObject非常像)。因为字符串本身是gc对象,所以Lua内部是通过一个字符串指针间接存储的。

真正的对象,实际是存储在Lua的global_state上一个全局字符串表里。这里可以看到和UE4的FName做法非常相似。

lua在创建字符串的时候,如果是小于40字节的字符串,就会调用上图的函数,先计算hash,并到全局的字符串表中查找,找到了就直接返回,没找到就新创建字符串,并保存在全局字符串表中。这里也可以看到连字符串插入的逻辑都和UE4的FName做法非常相似。

在短字符串做比较的时候,就直接比较字符串的指针,只要指针相等就认为字符串相同。这样的实现和FName直接比较Index做法非常相似。

看到这里,你肯定会想到,lua中的字符串就是一个FName的C语言版实现。游戏中的大量字符串,比如路径,对象名,在lua中和在NamePool中如果大量被使用到,就会在两边的字符串池中重复存储,这就造成了严重的内存浪费。lua的字符串池和UE4的NamePool,唯一不同的是lua的字符串会在没被引用时被GC销毁,且区分大小写。如果不在乎这两点区别的话,那么就完全可以使用FName来代替lua中的字符串,这样就可以让整个游戏只使用一份字符串内存(在乎大小写和GC销毁也有办法解决,就是会更麻烦一些,省下来的内存会少一些),相信很多项目,一定会加载大量的策划配置表中的字符串到内存中,最后又传入UE4被再保存一遍,如果砍掉lua的字符串存储,相信很容易就省下来大量内存(这些内存拿来多画几张贴图他不香吗?)。同时因为FName是UE4管理的对象,不需要lua参与gc,能够大幅度减少lua需要gc的对象数量,因此改造后也能显著提升lua的性能。

修改方法:

1 lstring.h,lstring.c中,修改下图的API对应实现,luaS_new创建时将FName的ComparisonIndex填入Value中直接返回,不要创建新的TString对象,将字符串当作值类型来使用,luaS_hash也可以改为CityHash

2 lstrlib.h lstrlib.c中的字符串函数,需要改为FName的对应版本,注意内部中间字符串不要创建新的FName,只在得到最终结果时再创建,否则因为NamePool很难清理,会出现大量临时FName污染NamePool。

3 全局搜LUA_VSHRSTR,通过Value获取TString的地方,根据情况修改对应实现,比如table的getkey和mainposition函数。加载字节码的lundump和保存字节码的ldump中保存字符串的地方等

最后,如果不想忽略大小写,可以打开UE4的宏,使用12字节的FName,这时因为lua的Value只能存8字节放不下,可以考虑做一个间接数组保存FName,将数组的index存到Value中。如果还需要让普通字符串参与gc,只让特殊字符串使用FName,可以在lua中,除了短字符串和长字符串外,再增加一种字符串类型,可以用特殊前缀(比如前面加一个@字符)来区分。

Unity3d理论上也可以用类似的做法,C#的string内部实现也差不多。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • FName
  • Lua中的字符串
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档