前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从一个170倍内存的优化说起脚本方案评估

从一个170倍内存的优化说起脚本方案评估

作者头像
车雄生
发布2022-04-01 20:57:14
9650
发布2022-04-01 20:57:14
举报
文章被收录于专栏:咩嗒咩嗒

一个170倍内存的优化

某一天,光子的一位童鞋突然拉了个小群,发了一段代码,然后发了几个测试数据,说测试结果和预期严重不符。大有一副“兴师问罪”的样子。

他们的测试代码是这样的:

测试数据是

  • 10w次内存27.9MB
  • 100w次内存135.5MB
  • 1000w次内存1027.8MB

我刚好当年测试过类似的用例,当时1000w当时800MB左右,我当时用的三个字段都是数字,光子童鞋其中一个字段用的是变长的字符串,多个200MB不过分吧?

我当时进入了思维误区,以为是过来兴师问罪,自然而然的认为内存占用过多了,继续交流下来才搞清楚是内存占用比预计低太多了,10W次的用例,光是字符串预计就占用4.65G。作为对比测试的unlua,在10W次的用例就崩溃了。预计指数级增长的用例,puerts测试到1000w内存占用还不到1个G。他们严重怀疑是测试代码写错或者获取内存占用数据的方式有问题。

很快地排除了测试代码本身的问题。于是数据获取方式也可以排除了,因为按那种估算,测试到1000w的时候世界任何一台电脑都会OOM。

所以基本上可以确定v8内部实现不是我们想象那样:字符串链接后的新字符串会保存所有字符。应该做了某种优化。

得益于网上有很多v8相关的分析文章 ,google搜索“v8 字符串”关键字,轻松就找到答案,比如这篇 ,看ConsString的部分,字符串连接后,产生的是一个ConsString实例,该对象仅需要两个指针指向被连接的两个字符串即可。最终测试用例那些字符串只需要一棵二叉树就可以表达。

对象存储效率对比分析

很早期我就做过这么个对比测试(对比的是lua54,而lua53的内存占用更高):

不少人问过为啥会有这差距,我最近和一位童鞋交流时写了段伪码来解释:

lua类似这样:

代码语言:javascript
复制
hashmap obj1;
obj1[key1] = value1;
obj1[key2] = value2;

hashmap obj2;
obj2[key1] = value1;
obj2[key2] = value2;

//obj3, obj4...

而v8类似这样

代码语言:javascript
复制
hashmap sharp;
sharp[key1] = offset(ObjectType.value1);
sharp[key2] = offset(ObjectType.value2);

struct ObjectType {
   hashmap* ptr_to_sharp;
   type1 value1;
   type2 value2;
};

ObjectType obj1(&sharp, value1, value2);
ObjectType obj2(&sharp, value1, value2);

//obj3, obj4...

区别在于lua的table(就一个hash表)需要存储key,value,而且hash表为了减少hash冲突,减少扩容次数(扩容记得是翻倍递增),往往会有一定的空间浪费。

而v8相当于每个对象只紧凑存储了value,所有同结构对象共享一套key到value的偏移信息,而且不同结构的偏移信息还可能可以继承,比如程序存在{x:0,y:0}和{x:0, y:0, z:0}两种对象,后者是前者的基础上增加z字段的偏移信息。

那lua比v8更耗内存?

也不是,得看场景。

启动一个v8虚拟机的基础开销(1~2M堆内存)要比lua(20K+堆内存)高,jit也有额外内存开销,所以很简单的逻辑,没有常驻内存的数据,lua会更有优势。

和一些重度使用lua脚本的游戏交流,有的项目能占到200~300M,有的项目会把策划配表加载到内存,光是策划配表就有80M,这时基础内存的占比就几乎可以忽略了,而虚拟机的一些内存使用效率优化的作用会凸显出来。

ConsString实际上极少那么极端的使用场景,影响不会有开篇那测试那么可观。而游戏中的策划配表,常用的面向对象编程,都会有数量比较多的同结构对象,v8这方面的优化感觉还是能节省下比较可观的内存。

还有一个不容忽视的事实是,v8的gc有做内存整理,而lua没有。lua长时间运行,除了纸面上能统计到的内存占用,还有那些虽然空闲,但因碎片化而使用不了的隐性占用。代码量小不等于内存少,事实上很多内存方面的精细化管理就需要复杂的代码来支撑。

但也不能因而得出大应用v8占优的结论,这和你的代码是怎么写有很大关系。具体问题具体分析,没有放之四海而皆准的结论。

性能测试建议

文章开头的光子童鞋做的是选型评估,除了内存,也包括性能的测试。

puerts基本不会和其它不同语言的方案去对比性能,但同时我也十分理解做技术选型的童鞋做性能对比,毕竟这几乎是唯一可量化的对比。我只是想提几点建议。

别只关注跨语言

我觉得这是最重要的建议,我看到的几乎所有对比测试都是着重于“跨语言”测试,我觉得很不合理,我觉得设计良好的代码,脚本的大部分代码应该都是在虚拟机内部运行,互相调用,跨语言占比比较少。因而虚拟机本身的性能对业务更为重要

早期各种lua方案间对比不需要对比虚拟机。但不同虚拟机还只关注跨语言,很可能会导致错误的导向。

也有部分选型测试测了虚拟机,但往往偏简单了:有的项目仅仅测试个fib,或者测试个加减乘除。业界也有语言性能对比方面的用例,可以参考下,比较常用的看这里,这些测试的主流语言实现看这里

还有不容忽视的GC的影响,做过unity开发的都会谈GC色变。GC的影响我觉得可以测试这两方面:

  • 常驻对象的影响
  • 临时对象的影响,Unty C#大家说的GC问题主要是这个,而lua没有分代GC的版本这块也是弱项,极端能让程序性能降几个数量级。lua5.4加入了分代GC,没仔细研究不做评论。

用例正交完备

何谓正交完备?

完备很好理解,就是覆盖要全,比如跨语言调用测试,要测试方法,属性,静态函数等等,数据类型要覆盖各种常用的类型。如果方案提供了多种调用方式(比如puerts同时提供了反射和静态两种),也建议都测试下。

正交,指的是这个用例测过了,别的用例就别测试了,每个用例的测试点不一样。

为何正交完备?

完备的用例有助于更全面的衡量,正交节省测试用例编写,也让测试数据更聚焦。

正交完备的用例不仅可以作为选型参考,还能指导我们后续的生产:

  • 性能估算:用过基础数据估算复杂接口的性能,比如可以粗略的认为:void (int, Vector) = void (int) + void (Vecot) - void ()
  • 为性能优化提供方向:如果一个Vector传输远大于3个float,那么在性能要求高的地方,可以把Vector参数改为3个float。

不过实践中,可能由于工作量的原因,很多项目的测试并不完备,这完全可以理解。完备但不是很正交的也可以理解,可能过于谨慎细致了。

但既不完备,也不正交,这就有点奇怪了:按说没时间,所以写不来那么多用例导致不完备,但又花大量时间反复的测一个数据类型是咋回事?我看到一套用例三分之一函数调用都在测试TArray引用,恰好用来测试的puerts版本TArray引用有点问题,比较慢。难道他们业务代码就是重度使用TArray,这样测试能衡量业务实际情况?于是我挺好奇的去了解下这是个什么样的业务,交流下才发现用例是源自unlua,这运动员兼职裁判兼得可以。

真有卷性能的必要么?

不是怕卷

在一套客观完备正交的用例下测试,puerts并不虚其它方案,事实上在大部分我所知的UE项目选型测试中,puerts整体性能占优。

我十分欢迎任何人搞这种PK,甚至疑似“不客观”的用例也帮助puerts发现了不合理的实现。

但我自己不会花精力在这块。

所谓质量,就是满足需求

仍是以那个TArray引用的问题为例,老版本存在值拷贝(new一个TArray实例,并把元素拷贝到新实例)两次的行为,改为传指针后测试数字上差距很大。但并没有项目真实使用中反馈之前的版本这块慢了,包括一些很重度使用ts的项目。

究其原因,要么都没用到(可以排除),要么那数字上的差距并没带来能感知的卡顿。这也是为啥一直没发现这两次值拷贝的原因。

我一直认为,性能够用的情况下,测试数字上提升带来的价值不大,当年华为请来的一位质量管理大师说过:“所谓质量,就是满足需求”。我深以为然。

跨语言这块,ue下的puerts的反射都比xlua的静态封装更快,而且puerts还有更快的静态封装。这是我对于ue下puerts跨语言“够用”的依据。

v8虚拟机性能“够用”的依据更多是来自“信任”:

  • v8在业界素以性能著称,超越不了所有脚本但也妥妥一线,猪场的荒野行动还用python呢
  • 相信大佬,v8项目的组长Lars Bak此前做过多个虚拟机(其中一个是java虚拟机,担任技术负责人),也相信以技术著称的Google背书。
  • 相信v8每天经过那么多代码,那么多平台的运行,不合理的性能短板应该都被发现并解决了。

不同虚拟机可比性不高

此类方案做的最主要的事情是跨语言访问,有人会把这块性能归功于“优化”得好,但这块很大程度上受限于脚本引擎提供的api以及语言特性,分别举个例子:

  • lua获取一个lua字符串的api,只仅仅返回一个内部的字符串指针,而v8却得你自己分配一段内存,让v8把字符串拷贝到该内存。
  • 引用参数的处理,在lua由于支持多返回值,引用参数输出时可以作为一个返回值,而js没有,puerts把参数装箱到一个js对象中,返回时把输出放到这个js对象,这意味着多了一个js对象的创建。

即使同样是js,不同实现提供的接口差别很大。比如苹果的jscore每个api全局加锁,它和原生交互就比v8要慢一个数量级,而v8嵌入api基本不会对外暴露数据结构,也不会让外部直接持有指针,而是通过句柄持有,传输数据用值拷贝。。。这些设计让其API相对lua会慢些。

别忘了还有C++

最后,如果性能不够用?性能要求高的地方为啥不直接用C++呢?从实践来看,性能要求高的地方往往需要更新的概率低。

ps,不久puerts会全平台支持wasm,到时会有更高性能的热更新选项。

何必卷纸面性能

性能不能满足需求时,券性能对项目是有帮助的,超越了需求的性能提升,我更愿意称之为纸面性能。

当然,是不是纸面性能也不是我说了算,如果项目使用的过程,发现哪些地方的性能“不够用”,我们也会快速响应及优化。我希望用具体需求来驱动优化而不是PK驱动优化

与其卷纸面性能,不如puerts把更多的精力投在其它方面,比如:

  • 对接好v8现有的工具链,让用户在编码,调试,自动化测试等有更好的体验;
  • 帮助用户写更健壮的代码,比如加入减少野指针访问的对象声明周期跟踪功能,比如更完美的支持ts的静态类型检查;
  • 尝试提升和UE引擎配合度,降低UE程序员的使用门槛
  • 尝试复用nodejs生态海量组件(实现cjs规范,甚至是直接对接嵌入式nodejs),让用户更专注于业务逻辑;
  • 尝试引入web领域,特别是UI界面方面的成功实践,比如react;
  • 将会尝试探索puerts在编辑器扩展上最佳实践;
  • 将会尝试营造一个游戏专属的组件生态;
  • 。。。

总结下观点

  • 我们的性能测试往往忽视了最重要的部分:虚拟机性能
  • 即使是大家最关注的跨语言测试,也往往做不到完备、客观
  • 除了性能,内存也非常关键,甚至更关键,个人排序:内存 > 虚拟机运算性能 > 跨语言
  • 性能满足需求后,纸面数据的提升并不能带来太多价值,应该多关注其它方面
  • 以用户价值驱动优化

有时我会反感此类测试,就好比评价一个手机的好坏仅仅看CPU跑分(无视了GPU,内存等各种其它硬件,也不关心设计,稳定性,质量等等),而且往往该跑分的程序还不足以全面衡量CPU。对于决策者是省事,用小学算术就能做决策,但这未必和实际使用相符,而且容易被钻空子:做手机的针对跑分程序优化显然最划算,如果跑分程序是自己写的就更稳了。

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一个170倍内存的优化
  • 对象存储效率对比分析
  • 那lua比v8更耗内存?
  • 性能测试建议
    • 别只关注跨语言
      • 用例正交完备
        • 何谓正交完备?
          • 为何正交完备?
          • 真有卷性能的必要么?
            • 不是怕卷
              • 所谓质量,就是满足需求
                • 不同虚拟机可比性不高
                  • 别忘了还有C++
                    • 何必卷纸面性能
                    • 总结下观点
                    相关产品与服务
                    对象存储
                    对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档