欢迎回来
登录你的知识库账户
忘记密码?
还没有账户?立即注册
创建账户
注册你的专属知识库
已有账户?去登录
找回密码
输入注册邮箱获取验证码
返回登录
请输入图片中的验证码以继续注册
加载中...
取消
新建收藏
手动添加你喜欢的内容
取消
编辑头像与昵称
上传新头像或修改你的显示昵称
支持 JPG/PNG,最大 2MB
取消

问题反馈

notebasewww.notebase.cn
控制台
内容库
动态
管理
账户
U
用户
--
在线
v0.8.7 · 知识库
笔记
KnowledgeBase
网络无边,知识有迹。
0笔记
0工具
30推荐

分类导航

按主题直达

编辑精选

站内用户贡献 · 真实笔记

最新收录

每日更新
继续浏览全部内容 →
>
笔记
0
加载中...
工具
0
此页用于记录用户反馈问题后的每一次改进
笔记用法

“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。

md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。

要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。

工具用法

在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。

要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。

工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。

隐藏笔记

写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。

适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。

数据安全

你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。

技术

全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。

理念

这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。

原则

不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。

更多

产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。

举报

如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。

趋势
// 点击导航加载发现
归档
// 归档为空
最近浏览
// 暂无浏览记录
发布
// 加载中...
用户发布
// 加载中...
用户管理
// 加载中...
访问统计
// 加载中...
内容审核
// 加载中...
个人信息
// 加载中...
返回首页

How I cut GTA Online loading times by 70%

1970/1/1技术杂谈

{"category":"游戏动漫","title_cn":"深入剖析:我是如何将GTA Online加载时间缩短70%的","content":"> 通过逆向工程和性能分析,找出GTA Online加载缓慢的根源——低效的JSON解析和二次方复杂度数组查找,并验证了修复方案。

去年,我重新拾起GTA Online,想完成几个新出的抢劫任务。不出所料,它的加载速度依然和七年前发布时一样慢得令人发指(反讽)。是时候一探究竟了。

初步侦察

首先,我检查了是否已有人解决此问题。我找到的大多数结果都指向一些道听途说:比如游戏太复杂所以加载慢、P2P网络架构垃圾(并非说它不垃圾)、各种从故事模式进入线上模式的复杂方法、或者跳过R星logo的Mod。进一步阅读后得知,将这些方法结合起来,我们最多能节省30秒!然而与此同时,在我的PC上……

  • 故事模式加载时间:约 1 分 10 秒
  • 线上模式加载时间:约 6 分钟(从R星logo到进入游戏,不含社交俱乐部登录时间)

我的配置虽老但尚可:AMD FX-8350 CPU、金士顿SA400S37120G入门级SSD、8GBx2 DDR3-1333内存、GTX 1070显卡。我知道配置过时了,但究竟是什么原因导致线上模式的加载时间是故事模式的6倍?我尝试了前辈们的故事转线上技巧,但无法测量到任何差异。即使有效,结果也在误差范围内。

问题并非个例

如果这个投票可信,那么该问题已影响超过80%的玩家,足以引起广泛不满。R星,这都7年了!

我研究了一下那些幸运的20%玩家是如何做到3分钟以内的加载时间。我找到了一些高端游戏PC的基准测试,线上模式加载时间约2分钟。我宁愿“黑”进去也要2分钟的加载时间!这看起来确实与硬件有关,但有些地方说不通——为什么它们的故事模式加载仍需近1分钟?而且,从故事模式加载到线上,他们只需多等1分钟,而我却要多等5分钟。我知道他们的硬件好得多,但绝不可能好5倍。

高度精确的测量

凭借任务管理器这样的强大工具,我开始调查哪个资源是瓶颈。

在花了一分钟加载故事模式和线上模式通用的资源后(这部分与高端PC相当),GTA决定在我机器的一个核心上满载运行4分钟,然后什么都不做。

  • 磁盘使用率? 0%
  • 网络使用率? 有一点,但几秒钟后就基本降为零(除了加载轮播信息条)。
  • GPU使用率? 0%
  • 内存使用率? 完全平坦……

这难道是在挖矿吗?我闻到了代码的味道。非常糟糕的代码。

单线程瓶颈

虽然我的老AMD CPU有8个核心,力量不弱,但毕竟是老古董了。当年AMD的单线程性能远落后于Intel。这可能无法解释全部的加载时间差异,但应该能解释大部分。奇怪的是,它只吃CPU。我原本预期会看到大量磁盘读取或网络请求来协调P2P会话。但这样?这很可能是个Bug。

性能剖析

性能分析器是查找CPU瓶颈的好工具。但有个问题——大多数分析器依赖插桩源码来获取进程内部情况的精确图像。我没有源码。我也不需要微秒级的精确读数——我的瓶颈持续足足4分钟。

于是,栈采样登场:对于闭源应用,唯有一个选项——按设定间隔转储运行中进程的调用栈和当前指令指针位置,构建调用树,然后汇总统计。

我知道的能在Windows上做到这点的分析器只有一个(可能我孤陋寡闻),而且它超过10年没更新了。它就是 Luke Stackwalker!拜托,哪位大神给它点爱吧!

通常Luke会合并相同的函数,但由于我没有调试符号,只能目测邻近的地址来判断是否是同一位置。我们看到了什么?不是1个瓶颈,而是2个!

深入兔子洞

借了我朋友那份完全正版的行业标准反汇编器(不,我真的买不起那玩意儿……总有一天我要学学Ghidra),我开始拆解GTA。

大多数知名游戏都内置防逆向工程保护,旨在阻止盗版、外挂和MOD制作者。但这从来都挡不住他们。看起来这里有某种混淆/加密机制,把大部分指令替换成了乱码。

别担心,我们只需在游戏执行目标代码时转储其内存即可。指令在执行前无论如何都需要去混淆。我手头有Process Dump,就用了它,不过也有很多其他工具可以做这件事。

问题一:这……是strlen?!

反汇编现在混淆度较低的转储后发现,其中一个地址的标签竟然是从某处提取的!是 strlen?沿着调用栈往下,下一个函数标签是 vscan_fn,后面的标签就没了,不过我相当确信那是 sscanf。

它在解析什么东西。解析什么?

理顺汇编代码会花很长时间,所以我决定用x64dbg从运行进程中转储一些样本。经过一番调试单步执行,结果发现它解析的是……JSON!整整10兆字节的JSON,包含约63,000个条目。

..., \"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 Online中所有可购买物品和升级的清单。但是10兆?这不算大!用sscanf解析可能不是最优方案,但肯定不至于这么慢吧?

嗯……是的,这确实要花很长时间!(注:我承认自己并不知道大多数sprintf实现内部会调用strlen,所以不能完全责怪写这段代码的开发者。我以为它只是逐字节扫描并能在NULL处停下。)

问题二:用哈希……数组?!

第二个问题被发现在第一个问题旁边调用。在同一个if语句里,从这段丑陋的反编译代码可以看出:

(所有标签都是我标,不知道函数/参数的实际名称)

第二个问题?在解析完一个条目后,它被存储到一个数组(或者说内联的C++ list?不确定)中。每个条目大致长这样:

struct uint64_t *hash; item_t *item; entry;

但存储之前呢?它会遍历整个数组,逐个比较条目的哈希值,看这个条目是否已存在。对于约63,000个条目,如果我的数学没问题,那就是 (n^2+n)/2 = (63000^2+63000)/2 = 1,984,531,500 次检查。大部分是无用功。你有唯一的哈希值,为什么不用哈希映射(hash map)?我在逆向时给它命名为hashmap,但它显然不是哈希映射(not_a_hashmap)。

而且更有趣的是:加载JSON之前,这个哈希数组列表是空的。而JSON中的所有条目都是唯一的!他们根本不需要检查条目是否已存在!他们甚至有一个直接插入条目的函数!直接用那个不就完了!认真的吗,这什么玩意!?

概念验证

这固然不错,但除非我做个测试,否则没人会相信我能写个标题党帖子。计划如下:编写一个.dll文件,注入GTA,Hook某些函数,???,然后出成果。

JSON解析问题很棘手,我不可能真的去替换他们的解析器。用一个不依赖strlen的sscanf替换更现实。但有个更简单的方法——Hook strlen,等等……"}

编写使用方法
Markdown 格式 · Ctrl+Enter 确定
新建笔记
预览
数据表格
点击单元格编辑 · Tab 移动
A1fx
Sheet1
BIH1H2≡🔗</>
隐私提醒

取消
编辑工具
取消