[译]我是如何将GTA在线模式的加载时间缩短70%的 您所在的位置:网站首页 gta5玩久了卡 [译]我是如何将GTA在线模式的加载时间缩短70%的

[译]我是如何将GTA在线模式的加载时间缩短70%的

#[译]我是如何将GTA在线模式的加载时间缩短70%的| 来源: 网络整理| 查看: 265

[译]我是如何将GTA在线模式的加载时间缩短70%的

译注: 最近在网上发现了一篇有意思的文章, 一个国外大神受不了GTA5在线模式的加载时间, 一怒之下反汇编了GTA5的源码, 并最终发现了问题的原因是因为R星写了一段非常烂的代码来读取JSON! 随后大神制作了优化补丁将加载时间缩短了70%, 并开源在GITHUB上! 他将从定位问题, 分析问题, 到解决问题的完整过程记录下来写成了一篇干货满满的技术文章. 文章用词幽默, 充满了对R星的吐槽, 一经发出很快登上了HackerNews的排行榜, 可见其热度.

WAKU将其完整地翻译为中文, 供大家学习交流, 翻译使用意译方式, 水平有限, 有错误请指出:)

原文地址: https://nee.lv/2021/02/28/How-I-cut-GTA-Online-loading-times-by-70/

原作者: T0ST

日期: 2021-02-28

GTA的在线模式.以漫长加载时间而臭名昭着.当我再次进入游戏来完成一些新的抢劫任务时,我震惊地发现它仍然像7年前发布的那天一样慢.

是时候了.是时候来研究下这个问题了.

#侦察

首先,我想看看是否有人已经解决了这个问题.我发现的大多数结果都是些个人经验, 说游戏如此的复杂,需要加载这么长时间,和一些说P2P架构如何垃圾的故事(不是说它不是),也有建议先加载故事模式然后再进入在线模式, 还有能在启动时跳过R星那个LOGO视频的Mod.继续深入的阅读,我发现可以通过组合这些方法来节省10到30秒!

此时在我的电脑上......

#测试 故事模式加载时间:~1分10秒 在线模式加载时间:~6分钟 启动菜单禁用了,从R*的LOGO一直到进入游戏(未计算社交俱乐部登录时间). 老款但正经的CPU:AMD FX-8350 便宜的SSD:金士顿SA400S37120G 必须得有的内存:两条 金士顿 8192 MB(DDR3-1337)99U5471 不错的GPU:NVIDIA GeForce GTX 1070

我知道我的配置过时了,但为啥需要6倍的时间才能进入在线模式?我用"先故事, 然后在线"这种加载技术也看不出有任何区别, 之前其他人已经做过类似测试. 即使这招确实好使,结果也不会很明显.

我(并不)孤单

如果这个调查可信,那么这个问题就足以让超过80%的玩家恼火.7年了, R星!

在四处寻找看谁是那20%能在3分钟内加载完的幸运儿时, 我看到了用高端游戏PC进行的一 些 测试, 能达到大约2分钟的加载时间!2分钟!让我死吧! 看起来硬件似乎是关键,但事情并不是这么简单......

他们的故事模式为何仍然需要加载近一分钟?(随便说一下M.2那个没有计算启动LOGO的时间.)另外, 从故事到在线的加载时间只花了他们1分多, 而我是5分多.我知道他们的硬件规格更好,但肯定没好到5倍.

#高精度测量

借助任务管理器这种强大的工具, 我开始调查哪块儿可能是瓶颈.

在花了一分钟用来加载故事和在线模式使用的共同资源后(这时间与高端PC差不多), GTA决定用4分钟挑战一下我电脑单核的极限,除此之外就没有别的了.

磁盘使用?没有!网络使用?有一点,但在几秒钟后,它基本上下降到零(除了加载那个旋转的信息横幅). GPU使用?零.内存使用情况?平常平稳......

那是什么呢,是在挖矿吗还是什么?我感觉到了一些代码.非常糟糕的代码.

#单线程

虽然我的旧AMD CPU有8个核心而且工作良好,但它是以前生产的.在AMD的单线程性能落后于英特尔的年代.这可能没法解释所有这些加载时间的差异,但应该能解释大部分了.

奇怪的是它只使用CPU.我本来以为会有大量的磁盘读取或者在P2P网络中进行频繁的网络请求.但瞅现在这个德性? 应该是有BUG了.

#分析

分析器是寻找CPU瓶颈的一种好方法.但是有一个问题 - 它们中的大多数都依赖于源代码来洞悉进程中正在发生的事情.我没有源代码.而我也不需要微秒级完美的读数 - 我有4分钟的瓶颈呢.

使用堆栈采样:对于没有源代码的应用程序,只有这一个选项.定期转储(Dump)正在运行进程的堆栈和当前指令指针的位置来创建一个调用树.然后将它们添加到当前的统计信息中.我只知道一个能在Windows干这个事儿的分析器(可能孤陋寡闻了).它已经超过10年没更新了.它就是Luke Stackwalker!有没有人, 拜托了, 请给这个项目一些爱:)

通常Luke会将相同的函数分组在一起,但是因为我没有调试符号,我不得不用肉眼来看周围的地址,以猜测它是否是同一个.我们看到了啥?不是一个瓶颈,而是俩!

#深入虎穴

借用了我朋友的业界标竿的正版反汇编器(不,我确实负担不起这玩意......我这两天得学学Ghidra了(译注:一个开源的逆向工程工具)),我把GTA开了瓢.

看起来不太妙啊.我们知道大多数知名游戏都有内置保护,防止逆向工程,以远离盗版,作弊器和修改器.尽管也没怎么防住.

这里似乎有某种混淆/加密,使用花指令替换了大多数正常的指令.不过不用担心,我们只是需要在游戏运行我们关心的那块儿时转储游戏的内存. 而在执行之前, 这些指令肯定是要还原为正常指令的. 我正好手头有Process Dump, 所以我用它了, 但是有很多其他的工具也可以完成这个事儿.

#问题1: 就是… strlen?!

通过反汇编该"轻微混淆"的转储文件显示, 其中一个地址被打上标记了!这是strlen?沿调用堆栈向下找, 下一个被标记的vscan_fn,再之后,标签结束了,但我很自信它应该是sscanf.

这是在解析什么东西.解析啥呢?跟这些反汇编纠缠起来没完没了, 所以我决定使用x64dbg来转储一些进程的采样.在一些调试步进后, 结果出来了那就是......JSON!他们正在解析JSON.一个有6万3千个项目的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"] },

这是什么?根据一些信息,它似乎是“网络商店目录”的数据.我假设它包含你可以在GTA在线模式购买的所有可能项目和升级的列表.

这里澄清一下:我认为这些是游戏中可购买的物品,与微交易没关系.

但10MB?没事儿!使用sscanf可能不是最优的,但肯定不是那么糟糕?好吧…

是的,这会花一段时间......公平的讲,我之前也不知道大部分sscanf的实现都调用了strlen,所以我也不能怪罪写这个的开发者.我会假设它只是一个字节一个字节的扫描,碰到NULL后停止.

#问题2: 让我们使用哈希- ... 数组?

看起来第二个罪魁祸首是紧接着第一个被调用的. 从这个丑陋的反编译代码中能看到它们是在同一个if语句里被同等调用的.

所有的标签都是我起的,不知道实际调用的函数/参数是什么.

第二个问题?在解析一个项目后,将它存储在数组中(或内联的C++列表?不确定).每个条目看起来长这样:

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

但在存储之前?它一个接一个地检查整个数组,将项目的哈希值进行比较,以检查它是否已经在列表中.大约有6万3000个项目,如果我没算错的话就是(n^2+n)/2 =(63000^2+63000)/2 = 1984531500次检查.绝大多数检查都没有用. 你已经有了唯一的哈希值为什么不使用哈希表.

我在逆向的时候将它命名为"哈希表",但显然它"不是一个哈希表".更绝的是.在加载JSON之前,这个"哈希数组列表"是空的.JSON里所有项目都是唯一的!他们甚至不需要检查它是否在列表中!他们甚至可以直接插入项目!用啊!真是的, 搞毛呢!?

#可行性验证(PoC)

挺好,但是没人会把我当回事, 除非我测试一下,这样我就可以给这个帖子起个骗点击的标题.

计划?写一个.dll,注入进GTA,hook一些函数,???,获利

JSON问题有点棘手,我无法实际替换他们的解析器.用一个不依赖strlen的sscanf更现实一些.但是还有一种更简单的办法.

hook strlen函数 等待一个长字符串 "缓存"它的开始位置和长度 如果在字符串范围内被再次调用的话, 返回缓存的值

例如:

size_t strlen_cacher(char* str) { static char* start; static char* end; size_t len; const size_t cap = 20000; // 如果我们已经"缓存"了这个字符串并且当前指针在它里面 if (start && str >= start && str cap) { start = str; end = str + len; } // 慢, 无聊的返回 return len; }

至于"哈希数组"的问题,它更加简单 - 只需完全跳过重复检查,直接插入项目,因为我们知道这些值是唯一的.

char __fastcall netcat_insert_dedupe_hooked(uint64_t catalog, uint64_t* key, uint64_t* item) { // 不用费劲逆向结构了 uint64_t not_a_hashmap = catalog + 88; // 不清楚这是干啥的, 把原函数的代码复制过来了 if (!(*(uint8_t(__fastcall**)(uint64_t*))(*item + 48))(item)) return 0; // 直接插入 netcat_insert_direct(not_a_hashmap, key, &item); // 当最后一个哈希命中时移除钩子 // 并且卸载.dll, 我们完活了 :) if (*key == 0x7FFFD6BE) { MH_DisableHook((LPVOID)netcat_insert_dedupe_addr); unload(); } return 1; }

可行性验证完整代码在这里.

#结果

所以, 好使了吗?

原在线模式加载时间: 大概6分钟 只打了重复检查补丁的时间: 4分30秒 只打了JSON解析器补丁的时间: 2分50秒 两个都打的时间: 1分50秒 (6*60 - (1*60+50)) / (6*60) = 69.4% 加载时间改善(棒!)

我去,成功了! 😃)

也有可能这不会解决所有人的加载时间 - 不同系统可能还有其他瓶颈,但这是一个如此巨大的漏洞,我不知道为什么R*这些年来都没有注意到.

#长求总 在启动GTA在线模式时有一个单线程CPU瓶颈 看起来GTA在解析一个10MB的JSON文件时挺费劲 JSON解析器自身实现的很烂, 并且 在解析之后有一个很慢的项目去重步骤 #R*请修复吧

如果Rockstar看到了本文:这个问题一个开发应该用不了一天就能解决.干点事儿吧 :



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有