3月初,游戏玩家tostercx宣称发明了一种新方法,可以把《侠盗猎车手Online》(检查GTA5)的加载时间缩短70%。对玩家群体来说,这无疑是一个好消息。

一直以来,《侠盗猎车手Online》因慢如蜗牛的加载时间已经“臭名昭著”。社交媒体Reddit上有一份针对这个问题的调查,结果发现超过80%的玩家都受到加载时间太慢的困扰。关键是,这个问题已经存在了7年,而游戏开发商RockstarGames却没有给出任何解决方案。

而tostercx决心深究,他发现加载时间慢的问题在于启动《侠盗猎车手Online》时存在单线程CPU瓶颈,并且游戏在费劲地解析10MB的JSON文件。对此,他认为,解决这款游戏加载时间慢的问题,只需要一名开发人员不到一天的时间。

随后,他写了一篇技术文章发在博客上,引起很大反响,不仅被HackerNews置顶,而且网友纷纷转发,甚至获得游戏开发商Rockstar10000美元的奖励。

Rockstar在官方通告中称,“经过彻底的调查,我们可以确定玩家tostercx确实揭示了《侠盗猎车手Online》PC版加载时间有待改善的游戏代码问题。作为调查结果,我们做了一些改进,这些改进会在更新中得到应用。”

慢如蜗牛的加载时间

作为《侠盗猎车手Online》玩家,tostercx不久前又上线打了几个任务,但是他发现这款游戏的加载时间仍然和7年前刚发布的时候一样慢。

但是,这些办法结合起来只能节省10-30秒。而他玩这款游戏,所需的故事模式加载时间是1分10秒,而在线模式加载时间是6分钟。详情如下:

故事模式加载时间:~1m10s在线模式加载时间:~6m左右禁用启动菜单,从R*logo到进入游戏内的时间(socialclub登录时间不算).老当益壮的CPU:AMDFX-8350便宜的SSD:KINGSTONSA400S37120G当然得有内存:2xKingston8192MB(DDR3-1337)99U5471不错的GPU:NVIDIAGeForceGTX1070

复制代码

tostercx在博客写道,“我知道我的机器配置已经过时,但是加载到在线模式竟然需要6倍左右的时间?即使用上别人总结的从故事模式切换到在线模式的加载技巧,也没产生什么效果。”

开始调查

Reddit的一份调查表明,超过80%的玩家都受到这个问题的困扰。

只有20%的玩家所用的加载时间不到3分钟,而他们极有可能是因为配备了高端游戏PC。根据tostercx的基准测试,高端游戏PC加载在线模式,只需要大约2分钟。

tostercx发现:即使如此,他们的故事模式还是需要近1分钟的加载时间?他们从故事模式切换到在线模式只需一分多钟。

“我知道,他们的硬件配置要好很多,但肯定不会好5倍那么厉害。”他说。

继续研究

利用任务管理器之类的强大工具,tostercx开始研究哪些资源可能成为瓶颈。

游戏花了一点时间加载用于故事模式和在线模式的通用资源,这部分时间与高端PC的耗时差不多。然后,游戏在他的计算机上拉满一个核心跑4分钟时间,这4分钟其他什么事都不干。

正如上图所示,磁盘利用率为0,网络利用率在几秒钟后也降为0,GPU利用率为0,而内存使用,则完全是一条直线。

tostercx表示,“它是在挖掘加密货币还是在干什么勾当?我闻到代码的味道了。真的是很糟糕的代码。”

问题根因

在tostercx看来,虽然自己的机器很老(老旧的AMDCPU有8个核心),制造时间也有很久,但是这可能无法解释所有的加载时间差异。

让他感到奇怪的是,游戏只占用了CPU资源。他以为会有大量的磁盘读取过程来加载资源,或很多网络请求负载尝试在p2p网络中协商会话。

“但是现在这样?这可能是一个bug。”

Profiling

Profiler(分析器)是查找CPU瓶颈的好工具。这里只有一个问题——大多数Profiler都需要检测源代码,才能对程序运行过程中所发生的事情有一个完整的了解。

而tostercx没有源代码,也不需要微秒级的读数——其瓶颈长达4分钟。

然后,了解一下堆栈采样:对闭源应用程序来说,Profiler只有一个选项。转储正在运行的进程的堆栈和当前指令指针的位置,以按设置的时间间隔构建一个调用树。然后将它们加起来以获取当前状况的统计信息。

据他了解,只有一个Profiler可以在Windows上执行这些操作,而且它已经十多年没有更新了。它就是LukeStackwalker!

一般来说,Luke会将相同的函数归为一组,但由于他没有调试符号,因此不得不盯着附近的地址来猜测它是否在同一位置。那么我们看到了什么?不是一个瓶颈,而是两个!

继续深入

tostercx借用了其朋友的行业标准级反汇编程序的完全合法副本,然后把GTA拆开来看个究竟。

这根本不对头。大多数著名游戏都带有针对逆向工程的内置保护措施,可以防止盗版、作弊和修改。这倒不是说这类措施一定有用。这里似乎存在某种混淆/加密,已经用乱码代替了大多数指令。

但是,这不是关键,“我们只需要在执行我们要看的部分时转储游戏的内存即可”。在运行之前,必须对指令进行混淆处理,他使用了ProcessDump。

问题一:这是……strlen?!

反汇编现在不太混乱的转储会发现,其中一个地址的一个标签被拉出到了某个地方!这是strlen?在调用堆栈中,下一个标记为vscan_fn,此后标记结束。tostercx认为它就是sscanf。

它正在解析某些内容。解析什么?反汇编太花时间了,因此他决定使用x64dbg从正在运行的进程中转储一些样本。后来经过一些调试步骤,他发现它是……JSON!他们正在解析JSON,一个带有约63k项的条目,体积高达10MB的JSON。

,{"key":"WP_WCT_TINT_21_t2_v9_n2","price":45000,"statName":"CHAR_KIT_FM_PURCHASE20","storageType":"BITFIELD","bitShift":7,"bitSize":1,"category":["CATEGORY_WEAPON_MOD"]},

复制代码

它是什么?根据一些参考资料,它似乎是“在线商店目录”的数据。它可能包含了你可以在GTAOnline中购买的所有物品和升级的列表。

问题二:使用哈希数组吗?

原来第二名=个罪犯和第一个是紧挨着的。就像在这段丑陋的反编译内容中看到的那样,它们甚至都在相同的if语句中被调用:

第二个问题?解析项目后,它立即存储在一个数组(或一个内联的C++列表?不确定)中。每个条目如下所示:

struct{uint64_t*hash;item_t*item;}entry;

复制代码

但是在存储之前?它会逐一检查整个数组,对比项目的哈希值以查看它是否在列表中。条目总共有约63k,也就是说需要(n^2+n)/2=(63000^2+63000)/2=1984531500。可是大多数操作都没用。tostercx表示,“既然你有唯一的哈希,为什么不使用哈希图呢?”

他在反编译时将其命名为hashmap,但它显然not_a_hashmap。这还没完。加载JSON之前,hash-array-list-thing是空的。而且JSON中的所有项目都是唯一的!他们甚至不需要检查它是否在列表中!它们甚至有一个直接插入项目的函数!就用它就行了!有没有搞错!?

解决方案

tostercx在找到问题后,写了一个**.dll,将其注入GTA中,并hook上一些函数**,搞定加载时间慢的问题。

JSON问题比较棘手,他无法替换它们的解析器。用不依赖strlen的sscanf替换sscanf更为现实。但是有一种更简单的方法。

hookstrlen

等待一个长字符串

“缓存”它的起始和长度

如果在字符串范围内再次调用它,则返回缓存的值

如下:

size_tstrlen_cacher(char*str){staticchar*start;staticchar*;size_tlen;constsize_tcap=20000;//ifwehavea"cached"stringandcurrentpointeriswithinitif(startstr=startstr=){//calculatethenewstrlenlen=-str;//ifwe'renearthe,unloadself//wedon'twanttomesssomethingelseupif(lencap/2)MH_DisableHook((LPVOID)strlen_addr);//super-fastreturn!returnlen;}//counttheactuallength//weneedatleastonemeasurementofthelargeJSON//ornormalstrlenforotherstringslen=builtin_strlen(str);//ifitwasthereallylongstring//saveit'sstartandaddressesif(lencap){start=str;=str+len;}//slow,boringreturnreturnlen;}

复制代码

至于哈希数组问题,其实更简单——完全跳过重复检查并直接插入项目,因为我们知道值是唯一的。

char__fastcallnetcat_insert_dedupe_hooked(uint64_tcatalog,uint64_t*key,uint64_t*item){//didn'tbotherreversingthestructureuint64_tnot_a_hashmap=catalog+88;//noideawhatthisdoes,butrepeatwhattheoriginaldidif(!(*(uint8_t(__fastcall**)(uint64_t*))(*item+48))(item))return0;//insertdirectlynetcat_insert_direct(not_a_hashmap,key,item);//removehookswhenthelastitem'shashishit//,wearedonehere:)if(*key==0x7FFFD6BE){MH_DisableHook((LPVOID)netcat_insert_dedupe_addr);unload();}return1;}

复制代码

很快,这办法就奏效了,《侠盗猎车手Online》加载时间缩短70%。如下:

Originalonlinemodeloadtime:~6mflatTimewithonlyduplicationcheckpatch:4m30sTimewithonlyJSONparserpatch:2m50sTimewithbothissuespatched:1m50s(6*60-(1*60+50))/(6*60)=69.4%loadtimeimprovement(nice!)

复制代码

根据这名玩家的总结:

启动GTAOnline时存在单线程CPU瓶颈

事实证明,GTA原来在费劲地解析10MB的JSON文件

JSON解析器本身没做好,并且

解析后,有一个缓慢的重复项目删除流程

附:

玩家tostercx的PoC完整资料

官方更新