笔记
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)。
这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。
不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。
产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。
如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。
趋势
我是如何将GTA Online加载时间缩短70%的
通过逆向工程定位并修复GTA Online加载中的两个性能瓶颈(低效JSON解析与O(n²)哈希查重),将加载时间从6分钟降至1分50秒。
背景:七年之痒
GTA Online 以加载缓慢而臭名昭著。当我重新拾起这款游戏,想要体验一些新的抢劫任务时,我震惊地(讽刺地)发现它的加载速度依然和七年前刚发布时一样慢。是时候了。是时候一探究竟了。
侦察:已有的解决方案
首先,我检查了是否已经有人解决过这个问题。搜索结果大多指向一些经验之谈——比如游戏太复杂所以需要加载这么久,或者P2P网络架构很烂(我并不是说这不对),还有一些复杂的方法——先加载故事模式,然后再切换到单人战局,以及一些跳过R*启动Logo视频的Mod。
进一步阅读后,我得知这些方法加起来最多能节省 10到30秒。与此同时,在我的电脑上……
基准测试:我的配置与现状
| 项目 | 详情 |
|---|---|
| CPU | AMD FX-8350(老但尚可) |
| SSD | 金士顿 SA400S37120G(廉价) |
| 内存 | 2x 金士顿 8GB DDR3-1333 |
| GPU | NVIDIA GeForce GTX 1070(尚可) |
实测加载时间:
- 故事模式:约 1分10秒
- 在线模式:约 6分钟(已禁用启动菜单,从R* Logo到进入游戏,不计Social Club登录时间)
我的配置确实过时了,但到底是什么导致在线模式加载时间比故事模式慢 6倍?我尝试了网上流传的故事模式转在线模式的技巧,但测量不到任何差异——即使有用,结果也淹没在噪声里。
我不是一个人
如果这个投票可信的话,这个问题已经广泛到让超过80%的玩家感到轻微烦躁。七年了,R星!
我继续寻找那些幸运的、加载时间低于3分钟的约20%玩家,发现一些高端游戏PC的在线模式加载时间约为2分钟。我愿意为此不择手段!但这似乎与硬件有关,但有些地方说不通:
- 为什么他们的故事模式加载仍然需要近1分钟?(M.2那个测试甚至没算启动Logo时间)
- 为什么他们从故事模式切到在线模式只多花1分钟,而我却要多花5分钟?
我知道他们的硬件好很多,但肯定不至于好5倍。
高精度测量:任务管理器初探
借助任务管理器这样的强大工具,我开始调查哪个资源是瓶颈。在花费约1分钟加载故事模式和在线模式共用的公共资源后(这个速度与高端PC相当),GTA决定让我的单核CPU满负荷运行整整4分钟,而其他资源几乎闲置:
- 磁盘使用率? 零!
- 网络使用率? 有一点,但几秒后基本降为零(除了加载旋转信息横幅)。
- GPU使用率? 零。
- 内存使用率? 完全平稳……
这是什么,在挖矿吗?我闻到了代码的味道。非常糟糕的代码。
单线程瓶颈
我的老AMD CPU虽然有8个核心,但它诞生于AMD单线程性能远落后于Intel的年代。这可能不能解释所有的加载时间差异,但应该能解释大部分。奇怪的是,它只用了CPU。我原本预期会有大量的磁盘读取或网络请求(用于P2P会话协商)。但这样?这很可能是个Bug。
性能剖析:Luke Stackwalker
性能剖析器是寻找CPU瓶颈的好工具。但有一个问题——大多数剖析器依赖插桩源码才能精确描绘进程内发生了什么。而我没有源码。我也不需要微秒级的精确读数——我有长达4分钟的瓶颈可供观察。
于是采用栈采样方法:对于闭源应用,唯一的办法就是转储运行中进程的调用栈和当前指令指针位置,按固定间隔构建调用树,然后汇总统计。
在Windows上,据我所知(可能是我孤陋寡闻)只有一个剖析器能做到这一点,而且它已经超过10年没有更新了。它就是 Luke Stackwalker!有人能给这个项目一点爱吗?
通常Luke会把相同函数分组,但由于我没有调试符号,只能通过目测相邻地址来猜测是否为同一位置。我们看到了什么?不是1个瓶颈,而是2个!
深入兔子洞:逆向工程
借了我朋友的一份完全合法的行业标准反汇编器(不,我真的买不起那玩意儿……总有一天我要学Ghidra),我开始拆解GTA。
这看起来完全不对劲。大多数知名游戏都有内置的反逆向工程保护,用来阻止盗版、作弊器和Mod制作者。但这从来没能真正阻止他们。这里似乎有某种混淆或加密,把大部分指令变成了乱码。
不过别担心,我们只需要在游戏执行我们想查看的部分时转储其内存。指令在执行前必须以某种方式去混淆。我手头有Process Dump,就用了它,但还有很多其他工具可以做这件事。
问题一:竟然是 strlen?!
反汇编去混淆后的转储文件,发现其中一个地址的标签是从某个地方拉出来的!是 strlen?沿着调用栈往下,下一个是 vscan_fn,再往后标签就没了,不过我相当确信那是 sscanf。
它在解析什么东西。解析什么?
手动解开反汇编代码需要很长时间,所以我决定用x64dbg从运行中的进程里转储一些样本。经过一番调试单步跟踪,结果发现是…… JSON!他们在解析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可能不是最优解,但肯定不至于那么糟糕吧?
嗯……是的,那确实要花不少时间。
公平地说,我之前不知道大多数sscanf实现会调用strlen,所以我不能怪写这段代码的开发者。我以为它只是逐字节扫描,遇到NULL就停止。
问题二:用个哈希……数组?
结果第二个问题就在第一个问题旁边被调用。它们甚至在同一条if语句中被调用,就像这个丑陋的反编译代码所示:
(所有标签都是我起的,不知道函数/参数实际叫什么。)
第二个问题?在解析完一个条目后,它被存储在一个数组(或者内联的C++列表?不确定)里。每个条目看起来像这样:
c
struct {
uint64_t *hash;
item_t *item;
} entry;
但在存储之前?它会遍历整个数组,逐个比较条目的哈希值,看看是否已经在列表中。
大约63,000个条目,那就是 (n²+n)/2 = (63000²+63000)/2 = 1,984,531,500次检查,如果我没算错的话。其中大部分是无用的。你都有唯一哈希了,为什么不用哈希表呢?我在逆向时把它命名为hashmap,但显然它是not_a_hashmap。
更妙的是:在加载JSON之前,这个哈希数组列表是空的。而且JSON里的所有条目都是唯一的!他们根本不需要检查是否在列表里!他们甚至有一个直接插入条目的函数!直接用它就好了!
真的,搞什么鬼!?
概念验证:Hook 与 注入
这很好,但除非我测试一下,否则没人会认真对待我,这样我才能给帖子起个标题党的标题。
计划如下:写一个.dll,注入GTA,Hook一些函数,然后……赚钱?
JSON问题很棘手,我不可能真的替换掉他们的解析器。用不依赖strlen的sscanf替换掉会更现实。但还有更简单的方法:
Hook strlen,等待一个超大长度参数(比如0xFFFFFFFF),然后返回一个较小的值(比如1KB)。
这样,sscanf内部的strlen调用就会提前停止,解析速度就会大幅提升。虽然这会导致解析不完整,但对于我们的目的(加载时间优化)来说足够了。
对于哈希数组问题,我Hook了那个检查函数,让它直接返回“不在列表中”,从而跳过整个O(n²)的查重过程。
结果:70%的提升
注入我的.dll后,再次测量加载时间:
- 在线模式加载时间:从6分钟降至1分50秒
- 故事模式加载时间:基本不变(约1分10秒)
总计节省了约4分10秒,即70%的加载时间。
技术总结与反思
这个案例完美展示了几个关键点:
- 性能瓶颈往往不在你预期的地方——不是磁盘、不是网络、不是GPU,而是一个看似微不足道的CPU单线程操作。
- 算法复杂度是致命的——O(n²)的查重算法在63,000个条目时产生了近20亿次无意义的比较。
- 标准库的隐藏陷阱——
sscanf依赖strlen,而strlen在处理大字符串时本身就是一个O(n)操作,叠加在解析循环上变成了O(n²)。 - 逆向工程是有效的调试工具——即使没有源码,通过栈采样和反汇编也能定位并验证问题。
- Hook是快速验证的手段——不需要完美替换整个解析器,只需Hook关键函数就能验证瓶颈假设。
对游戏开发者的启示
- 如果你的游戏有类似的“商店目录”或“物品列表”,请使用哈希表或有序结构,不要用线性数组做查重。
- 解析JSON时,使用专门的JSON库(如RapidJSON、nlohmann/json),不要用
sscanf这种通用解析函数。 - 在加载界面显示进度时,确保CPU没有被无意义的循环占满。
- 定期用剖析器检查加载流程,尤其是那些“只跑一次”的初始化代码。
对玩家的建议
- 如果你有技术能力,可以尝试类似的Hook方法来优化游戏体验。
- 但请注意,这违反了游戏的服务条款,可能导致封号。
- 更好的选择是向R星反馈问题,希望他们修复——虽然等了七年都没动静。
结语
这不仅仅是一个关于GTA Online加载时间的修复故事,更是一个关于性能分析思维的案例。当我们遇到性能问题时,不要被表象迷惑(比如“P2P网络烂”或“硬件不够好”),而是要用数据说话,用工具定位,用逻辑推理。
一个看似无法解决的“游戏优化问题”,最终被证明是两个简单的代码错误。这提醒我们:有时候,最复杂的系统问题,根源可能简单得可笑。
原文链接:https://nee.lv/2021/02/28/How-I-cut-GTA-Online-loading-times-by-70/