# 真机验证清单:解压速度(10–40 MB/s 到底卡在哪) > ### ⚑ 本清单的终局:停在第 1 步之前(2026-09-23 18:15,用户决定) > > **`T_copy` 没做;RAR 多线程(#51)不做;生产代码一行不动、不刷机 —— 11 分钟维持现状。** > > 理由:MT 的收益在 PC 上**已实测**(下面「第三件」2.45×/我们引擎 1.40–1.74×), > 但**能不能兑现,刚好吊在本文档第 1 步那个实验上** —— 而 MT 只并行解码、写盘恒为主线程 > 串行(`unpack50mt.cpp` 的 `UnpWriteBuf()` 只在主线程调,见 §六 终局块), > 所以那个实验的结果很可能是「收工」。赌一次的成本(改构建 + 刷机 + 重跑 11 分钟 + 真机验证) > 压在不确定收益上 ⇒ **不做**。 > > **下面全部内容保留**,作为「将来重开时的现成答案」。 > ⚠️ 重开的**第一个动作是第 1 步的 `T_copy`,不是改构建**。 > ### ⚠️⚠️ 2026-09-23 深夜(第二次更正):**归档参数已拿到、MT 已实测、最大嫌疑已排除** > > **口径修正**:包不是 18 GB,是 **3 个分卷 ≈ 11.6 GB**(4 GB + 4 GB + 3.6 GB)。 > **下文凡写「18 GB」处,一律按「≈11.6 GB」读**;核心算术已在本块重算。 > > **① 真实归档参数(从 part3 的头实测,该文件随后被清理)** > RAR **5**、**卷 3/3**、**锁定**、**不是固实**(直解主头 `archive flags=0x0013`,`MHFL_SOLID=0x0004` 未置位)、 > 压缩参数 **`-m1 -md=4g`**(**最快档 + 4 GiB 字典**)。关键条目 `PPSA16608.exfat` > **19.49 GB → 打包 3.83 GB(ratio 5.09)**,并**整段都在 part3 内**。 > 详见 `docs/EXTRACTION-PERF.md` §六 第二次更正块。 > > **② 最大嫌疑(scan 白解码一遍 = 2×)已排除。** 那建立在「固实 skip 必须解码」 > (`dll.cpp:341` 只在 `!Arc.Solid` 走廉价 `SeekToNext()`)之上;**本档非固实** ⇒ 我们的 scan 不解码。 > > **③ MT 收益已实测 = ~2.4×**(真实形状夹具:`-m1 -md4g`、5:1 可压缩、2 GB、`-mt1` 386→`-mt8` 946 MB/s 解压后)。 > 接线只需 **1 个编译开关 + 1 个链接开关**(`-DRAR_SMP` + `-lpthread`),**不必改 vendored 源码、 > 不必增删源文件** —— 早期版本写的「还要加 `threadmisc.cpp`」**是错的**:`threadpool.cpp:5` 已经 > `#include "threadmisc.cpp"`(实测 `nm unrar7_threadpool.o` 里有 `T GetNumberOfThreads`),再加会重符号。 > > **④ 但矛头现在指向「PS5 写路径」——所以本清单第 1 件事(`T_copy`)不变,而且更该先做。** > - 单线程解码在 PC 上同形状就是 **386–505 MB/s**;PS5 单核按 1/3 算也 ~130 MB/s。 > - 真机:**只算 `.exfat` 一项**就是 19.49 GB ÷ 660 s = **≥29.5 MB/s 解压后**。 > - **两次独立操作撞同一个数**:上传(写 11.6 GB)**30–40 MB/s** = 解压(写 ≥19.5 GB)≈30 MB/s。 > ⇒ 优先怀疑这条写路径的上限就是 **30–40 MB/s**。**若 `T_copy` 也 ~10 分钟 ⇒ 收工,MT 不做** > (做了会被 I/O 吃掉);**若 `T_copy` 明显快 ⇒ 立刻上 MT,那 2.4× 是真的**。 > > 下面是原始判断(保留),**优先级以本块为准**。 > ### ⚠️ 2026-09-23 晚 更正 —— 本文档的起点判断已反转。 > 本文最初的前提是「已排除解码器」,那建立在两条现已作废的论证上: > ① 拿 **PS5 上解 RAR** 的 28 MiB/s 去比 **PC 上解 7z** 的 427 MiB/s(不同格式/解码器/机器); > ② UI 速度的 4× 摆动(已自行降级为 250 ms 采样噪声的弱证据)。 > > **新增的三条事实把方向翻了过来**:格式是 **RAR**(解码就是 rarlab 的 UnRAR 库本身); > 包原本在 PC 上、经插件上传进 PS5;上传实测 30–40 MB/s。随后查代码发现: > **我们的 POSIX 构建没有定义 `RAR_SMP`**(`os.hpp:43-45` 的 `#define RAR_SMP` 在 > `#ifdef _WIN_ALL` 内;官方 POSIX makefile 第 11 行是 `DEFINES=... -DRAR_SMP`), > 后果是 `unpack50mt.cpp`(`Unpack::Unpack5MT`,多线程 RAR5 解压器)**根本没编进来**, > `unpack.cpp` 的 MT 分支整段不参与编译 ⇒ **RAR 解码在 PS5 上是单线程的**。 > > ⇒ **新的首要假设:28 MiB/s ≈ 单线程 RAR5 解码的正常量级。** > 详细更正与代码行号见 `docs/EXTRACTION-PERF.md` §六 文首更正块。 > **下文按「实测 → 判读」仍有效;但第 2 步(读粒度)对 RAR 不适用**,已加注。 ## 已到手的数据(2026-09-23 真机) | 项 | 值 | |---|---| | 包大小 | **≈11.6 GB 压缩包**(4 GB + 4 GB + 3.6 GB 三个分卷;✅ 2026-09-23 晚更正,原写 18 GB 有误) | | 条目数 | 1252 | | 总耗时 | **11 分钟 = 660 s**(UI 显示) | | **UI 速度口径** | **解压后字节**(`src/rar_extract.c:766` 在 `UCM_PROCESSDATA` 里 `bytes_done += p2`)⇒ 10–40 MB/s 是「吐出数据的速度」 | | 平均吞吐(输入侧) | **≥ 17.6 MB/s 打包字节**(11.6 GB ÷ 660 s) | | 平均吞吐(输出侧) | **≥ 29.5 MB/s 解压后**——只算 `PPSA16608.exfat` 一项就是 19.49 GB ÷ 660 s | | 格式 | **RAR 5** ✅(头 8 字节 `52 61 72 21 1A 07 01 00`) | | 固实 | **不是固实** ✅(主头 `MHFL_SOLID=0x0004` 未置位) | | 压缩参数 | **`-m1 -md=4g`**(最快档 + 4 GiB 字典)✅ | | 关键条目 | `PPSA16608.exfat` 19 493 027 840 B → 打包 3 831 295 727 B(ratio **5.09**),整段在 part3 内 | | 来源 | 包原本在 **PC 上**,**经插件本身上传**进 PS5,上传速度 **30–40 MB/s**(写 11.6 GB) | | 源 / 目标设备 | **待确认** ⬅ **现在最关键的一条**(内置 SSD / 外置 USB / 是否同盘) | | 解压后总大小 | **待确认** ⬅ 决定输出侧吞吐的确切值(≥19.49 GB 已知) | 「18 GB 是压缩包大小」这条把口径钉住了,而且给出一个**不需要知道解压后大小的硬上界**: ``` 源盘必须在 660 s 内交出 18 GB 的归档数据 ⇒ 整条流水线的平均吞吐上界 = 18 GB ÷ 660 s ≈ 28 MiB/s ``` 解压后有多大都不影响这个上界 —— **无论解码多快,源盘就只有这个交付速度**。 所以第 1 步的 `T_copy` 和它**可以直接比大小**(同一个 18 GB 文件、同一对设备)。 两个立刻可用的推论: 1. **per-entry(小文件)开销解释不了它。** 1252 个文件、平均 14.7 MB,不是「几万个小文件」那种 形态;建文件 + rename 这两种 per-entry 成本加起来分摊到 0.53 s/文件 里微乎其微。 2. **UI 上那个 10–40 MB/s 是瞬时值,不是平均值。** 进度回调只在累计 ≥ 1 MiB 时上报 (`src/zip_extract.c:123`),`src/task.c:202-206` 每 **250 ms** 采一次样。250 ms 窗口 + 写缓冲突发会让速度在「忽停忽走」之间跳,**波动里含相当比例的采样噪声**。 ⇒ 只有「解压后字节 ÷ 总耗时」可信;那 4× 的摆动只能说明「不是恒定负载」,不能单独定案。 --- ## ★ 你要在真机上做的事(就两件) ### 第一件:补一个数字 `T_copy`(3 分钟操作 + 一次等待) 1. 把那个 **18 GB 的包**,用插件自己的**复制**功能,复制到**解压时输出所在的那块盘** 2. **记下耗时**(UI 上有) 3. 顺便记下:这个包**原本在哪**(内置 SSD / 外置 USB),**解压输出到哪**(同盘还是另一块) 判读(就在 `T_copy` 与 **660 s** 之间比): | `T_copy` | 结论 | 我接下来做什么 | |---|---|---| | **≥ 10 分钟** | 存储物理极限,与代码无关 | **收工**,解码优化全部关闭(#48 取消) | | **1–2 分钟** | 解压比同一对设备上的纯搬运慢 5–10× ⇒ **代码里有真问题** | 做 #48:`vol_read()` 加 read-ahead | | 介于中间 | 混合 | 按字节数扣掉 I/O 分量,差值才是能改的部分 | > 为什么这个数字这么关键:它是**同一台机器、同一对设备、同一份代码**,只把「解压」换成 > 「纯搬运」。不需要造新包、不需要第二块盘、不需要任何假设。上面那 4 个对照实验(第 3 步) > 只在它的结论模糊时才需要。 ### 第二件:两个小事实 —— **已答一半,剩两个待确认** 1. ~~RAR4 还是 RAR5?~~ **RAR5 ✅**;~~是否固实?~~ **非固实 ✅**;压缩参数 **`-m1 -md=4g` ✅** (都从 part3 的头上直接读到,见文首第二次更正块 ①)。 顺带确认:**UI 的口径是解压后字节**(`src/rar_extract.c:766`)。 2. **还缺两个数(都很便宜)**: - **解压后总大小** —— 决定输出侧吞吐。已知 ≥19.49 GB(仅 `.exfat` 一项)。 插件跑解压时 UI 上的 `total` 就是它(`task->total = p->bytes_total`,`src/extract.c:64`)。 - **源 / 目标设备** —— 包在哪块盘、解压输出写到哪块盘、是否同一块盘。 这一条现在比什么都重要:写路径被怀疑是墙(文首块 ④)。 ### 第三件(我已做完,不用上真机):多线程 RAR5 的收益 用 WinRAR 自带的 **UnRAR 7.23**(`-mt` 开关可解析,虽然不出现在 `-?` 帮助里) 在一份**按真实形状**造的夹具上做 A/B(`-m1 -md4g`、5.27:1 可压缩、2 GB、分卷): | 线程 | 耗时 | 解压后吞吐 | 加速 | |---:|---:|---:|---:| | 1 | 5.18 s | 386 MB/s | 1.00× | | 4 | 2.39 s | 835 MB/s | 2.16× | | **8** | **2.11 s** | **946 MB/s** | **2.45×** | ⇒ **收益 2.4×,且已在真实形状上验证**(非固实版本 2.42×,量级一致)。 接线只需 **1 个编译开关 + 1 个链接开关**(`-DRAR_SMP` / `-lpthread`),**不用改 vendored 源码、 不用增删源文件**(早期写的 `threadmisc.cpp` 那一条经实测作废,见 §六 块 ④ 行内更正)—— 原因与行号见 `docs/EXTRACTION-PERF.md` §六 第二次更正块 ④。 > ⚠️ 但结论顺序仍以第一件为准:**先 `T_copy`**。如果墙是 30–40 MB/s 的写路径, > 这 2.45× 会被 I/O 完全吃掉,做了等于白做。 > ⇒ **闭环(2026-09-23 18:15):`T_copy` 没做,用户决定不做这项了**(见文首终局块)。 > 本节的实测数据**保留** —— 将来重开时它就是收益依据;但**第一步仍是 `T_copy`**。 --- ## 第 1 步(决定性实验,= 上面「第一件」):用插件自己的「复制」功能做 I/O 基线 **这是目前唯一能把「存储」和「我们的代码」一刀切开的实验,而且零改动。** 插件里 **`TASK_COPY` 是已实现功能**,并且对 ≥ 256 MiB 的文件自动走 `copy_file_pipeline()`(`src/filemgr.c:836`,阈值见 `:37`):**3 个 slot × 8 MiB、4096 字节对齐、 独立读线程 + 独立写线程**。也就是说,它就是「同一台 PS5、同一对设备、同一份代码, 只把解压那一步换成纯搬运」。 **操作**:把那个 18 GB 的包,用插件自己的复制功能,复制到**解压时输出所在的那块盘**上。 记下耗时 `T_copy`。 **判读:** | 观察到 | 结论 | 下一步 | |---|---|---| | `T_copy` ≈ 10 分钟或更多 | **存储物理极限**,与我们的代码无关 | 收工。解码优化全部搁置 | | `T_copy` ≈ 1–2 分钟(远小于 660 s) | 我们的解压比同一对设备上的纯 I/O **慢 5–10×** ⇒ **代码里有真问题** | 进第 2、第 4 步 | | 介于两者之间 | 混合 | 按字节数把 I/O 分量扣掉,差值才是我们能动的部分 | 严格比较要看**总移动字节**:复制读 18 GB + 写 18 GB = 36 GB;解压读 ≤ 18 GB、写 = 解压后大小。 所以「复制 36 GB 用了多久」和「解压搬了 (18 GB + 解压后大小) 用了 660 s」才是同一口径。 > 为什么这个实验优于第 3 步那一堆:它不需要造新包、不需要两块盘、不需要任何假设, > 而且**测的就是出事的那对设备**。第 3 步只在第 1 步结论模糊时才需要。 --- ## 第 2 步:读写粒度审计 —— ~~目前唯一可疑的代码级病因~~(⚠️ **对 RAR 不适用**) > **2026-09-23 晚加注:本节整段只对 7z / ZIP 有效。** 它比较的是「引擎内部缓冲大小」, > 而 RAR 的归档 I/O 在 UnRAR 自己的 `File` 类里(我们按路径打开, > `src/rar_extract.c:1059`),我们的回调只收到解压后的数据 —— **读粒度我们改不了**。 > 唯一还能对上 RAR 的那半句是「**写入**粒度」,而 RAR 的写出也在 UnRAR 内。 > ⇒ **真机工作负载是 RAR 时,本节没有可操作性**;留着是为了 7z/ZIP 场景。 > 顺带:本表里 RAR 那行写的 4 MiB 是 `File::CopyBufferSize()` > (`third_party/unrar7/file.hpp:148-153`),那是**文件复制**的缓冲,**不是解压读取缓冲** —— > 原文引用错了行,这条勘误一并记在这里。 把四条路径的**单次请求大小**摊开看(已核对源码): | 路径 | 源侧读粒度 | 目标侧写粒度 | |---|---|---| | **复制**(pipeline,≥ 256 MiB) | **8 MiB** × 3 slot,4096 对齐,独立读线程 | 8 MiB | | **7z** | chain `SZ_IN_CHUNK = 256 KiB`(`src/sevenz_chain.c:87`);MT 路径 `inBufSize_MT = 1 MiB` | `SZ_OUT_CHUNK = 64 KiB`(`src/sevenz_chain.c:94`) | | **ZIP** | `ZIPX_IO_BUFFER = 128 KiB`(`src/zip_extract.c:32`) | 128 KiB(`src/zip_extract.c:900`) | | **RAR** | UnRAR 内部 `File::CopyBufferSize() = 4 MiB`(`third_party/unrar7/file.hpp:148-153`) | 4 MiB | ⇒ 结论一:**三个引擎都不是「小读」病理**,最小也有 128 KiB,不是 4 KB 那种灾难。 ⇒ 结论二:**但都比复制小 32–64 倍**。 为什么这可能正是病根 —— 在**延迟主导**的设备上,吞吐 ≈ 单次请求大小 ÷ 每次请求的等效延迟: ``` 256 KiB / 10 ms = 25 MB/s ← 正好落在实测 10–40 MB/s 的中间 8 MiB / 10 ms = 800 MB/s ← 复制路径不会撞这个上限 ``` 这个算术顺带解释两件事: - **为什么吞吐会 4× 摆动**:等效延迟随设备状态(HDD 寻道、USB 桥接、写缓存回刷)变化, 线性映射到吞吐上就是大幅波动。 - **为什么「条目平均 14.7 MB」没能救我们**:per-entry 成本的确不是主因,但**读请求粒度**是另一回事 —— 它由引擎内部缓冲决定,与条目大小无关。 最可能的场景是**源和目标在同一块盘**(外置 USB HDD 上解压到同一块盘):读流和写流同时存在, 磁头来回跑,OS read-ahead 被写回刷反复打断,于是每次请求退化成一次寻道 —— 这正好让上面那个 算术成立。第 1 步如果出现「复制明显快于解压」,就是在支持它(复制的寻道次数只有解压的 1/32)。 **如果第 1 步指向这里,改动面其实很小**:所有 7z 的读都只经过 `src/sevenz_volstream.c` 的 `vol_read()` 这一个函数(ZIP/RAR 同理在 `src/zipx_volstream.c`)。在那里加一层 read-ahead (向后 seek 时丢弃缓存)即可,**不需要动解码器**。但这要等第 1 步的结论,现在不写。 --- ## 第 3 步:四个对照实验(零改动)—— 第 1 步结论模糊时才需要 | 实验 | 做法 | 若结果为 A | 若结果为 B | |---|---|---|---| | **A 换简单包** | 造一个「**单一大文件**(如 10 GB 伪随机数据)」的 zip,放内置 SSD,解到内置 SSD | 吞吐跳到 **150+ MiB/s** ⇒ 慢在**条目数 / per-entry 开销** | 仍是 10–40 ⇒ 慢在**存储或 I/O 模式**,与条目数无关 | | **B 换源设备** | 同一个包,分别放**内置 SSD** 与**外置 USB** | 两者差别巨大 ⇒ 就是源盘带宽 | 两者一样慢 ⇒ 不是源盘 | | **C 换目标设备** | 同一个包,分别解到**内置**与**外置** | 差别巨大 ⇒ 写入端是瓶颈 | 一样慢 ⇒ 不是目标盘 | | **D 同盘 vs 异盘** | 包与输出在同一块盘 / 在两块盘 | 同盘明显更慢 ⇒ 读写争用(这也是第 2 步最看好的假设) | — | > 参考量级:机械/低成本 USB HDD 顺序读写约 30–80 MB/s,小文件随机访问降到 1–10 MB/s。 --- ## 第 4 步:只有在第 1/3 步指向「我们的代码」时才做(要改代码) 1. **阶段计时**:在四个阶段边界累加时间戳 —— `scan` / `read+decode` / `write staging` / `publish`。用 `clock_gettime(CLOCK_MONOTONIC, ...)`(该原语已在三个引擎里用于进度上报, 真机可用)。收尾用 `printf` 打进日志。 2. **拆开 `read+decode`**:这一段目前分不开。可用「同一归档跑两遍(第二遍吃页缓存)」或 「解到 /dev/null 类目标」来逼近 I/O 与解码的分界。 3. **`SZX_MT_THREADS` 运行时可配**:现在是 `src/sevenz_mt.h:21` 的编译期 `#define 8`, 做线程数 A/B 必须重编四次。改成运行时读(默认仍 8)会让实验便宜很多。 4. **读粒度 A/B**:`SZ_IN_CHUNK` / `ZIPX_IO_BUFFER` 加大到 1–4 MiB 各构建一版, 看第 1 步指出的那条曲线是否真的跟着动。 5. 埋点要用**编译开关**控制,关闭时零开销 —— 避免影响已发布的产物指纹。 --- ## 已排除的项(不要再花时间) > ⚠️ 本表 2026-09-23 晚已按「格式 = RAR」重排。原表里「解码整体就不是瓶颈」这条前提已撤回, > 所以**同一批项现在被分成两类**:与 RAR 无关(不用看)、以及真已排除。**两类都不要花时间。** **A. 与 RAR 工作负载无关**(它们是 7z / ZIP 的项;RAR 解码在 rarlab 库里,我们碰不到) | 项 | 为什么无关 | |---|---| | CRC 硬件化 | 对 7z/ZIP 才值 4–5%;**RAR 的 CRC 由 UnRAR 自己算** | | MT 扩到 BCJ2 / 多 folder | 纯 7z 概念 | | ZIP inflate 换 libdeflate | ZIP 专属 | | AES-NI | 只影响 ZIP 加密流与 7zAES;RAR 加密走 UnRAR 自己的 rijndael | | 读粒度 / read-ahead(#48) | RAR 按路径自读(`src/rar_extract.c:1059` 的 `od.ArcName`),回调只收解压后数据,**插不进去** | **B. 真已排除**(与格式无关,或已结论) | 项 | 为什么排除 | |---|---| | 与 7-Zip 比倍数 | 真机上没有可比对象(上游那个 `wfm-7zip-helper.elf` 单独分发,本仓刻意不走 helper 路线);而且**那是 7z 的对比,与 RAR 负载无关** | | 逐条目 fsync | 已经全部移除(2026-09-16) | | 「条目太小」 | 1252 条 / 18 GB,平均 14.7 MB,不是小文件场景 | | 「我们的 RAR 解码器写得慢」 | 解码就是 vendored 的 UnRAR 库本体,我们只剩一层 facade | **C. 唯一还站着的、且直接针对真实负载的一项** ⬅ **优先看这个** | 项 | 状态 | |---|---| | **UnRAR RAR5 多线程解压(`RAR_SMP` + `-lpthread`)** | **❌ 不做(2026-09-23 用户决定,见文首终局块)**。我们没打开它;收益已实测(PC 上 2.45× / 我们引擎 1.40–1.74×)但被「写路径是否为墙」卡住,而验证它的 `T_copy` 未执行 ⇒ 搁置 |