SFC 性能报告:v2.1.0 / v2.2.0 / v2.2.1
本文所有数字都用吞吐量表示,即每秒处理多少数据,单位 MiB/s,越大越好。不用耗时秒数,因为改数据量、改文件数都会让秒数失去可比性,吞吐量才是能横向对照的刻度。涉及小文件时另用文件/秒,因为此时瓶颈在文件个数而不在数据量。
1. 性能是由什么决定的
压缩一个目录,时间花在三件事上:
- 从硬盘把文件读进来
- 压缩、加密
- 把结果写到硬盘
程序里这三件事由不同的线程分别负责,可以同时进行。所以整体快慢不是三者相加,而是由最慢的那一件决定。
这里有个关键区别:第 2 件可以拆给多个核心同时做;第 1 件和第 3 件在程序里各自只有一个线程,加再多核心也快不了。由此可以直接推出本文最重要的结论:如果第 2 件已经比第 1、3 件快了,那么再优化压缩算法也不会让程序变快。当前就是这种情况,证据见第 3 节。
2. 测量条件
硬件为 AMD Ryzen 7 8845H,8 物理核 16 逻辑处理器,内存 23.3 GB。磁盘 8 GiB 顺序写 2880 MiB/s,顺序读 6775 MiB/s。
测试数据三组:
| 代号 | 内容 | 规模 | 特性 |
|---|---|---|---|
| NCIEP | 真实工程树,45,390 个文件 | 13.47 GiB | 近不可压缩,归档 13,430 MiB |
| cpu_corpus | 16 个文件 | 2,048 MiB | 高可压缩,归档 896 MiB,往返全都落在系统内存里 |
| fewbig | 16 个大文件 | 13,312 MiB | 随机不可压缩,规模与 NCIEP 相当,但没有逐文件开销 |
版本基准:v2.1.0 取标签 v2.1.0-windows-GUI,v2.2.0 取 2.2.0 代码线的末端 09144c1,v2.2.1 取 a4c44e5。三个版本按各自的发布编译选项构建,v2.1.0 与 v2.2.0 为 -O3 -DNDEBUG,v2.2.1 另外开启 LTO。版本对照统一使用 huffman-aes 模式。
两类测量分开进行,两者不可互相换算:
- 内核测量:把压缩与加密单独取出,单线程,每 8 MB 一块,反复测取最快的一次。
- 整体测量:走完整的读、算、写流程,包含文件系统、归档格式与磁盘 I/O。
多核数据通过环境变量 SFC_WORKERS 指定工人数。未设置时工人数取硬件并行度。
3. 当前性能
3.1 多核利用
以计算为主的数据测纯计算能力:cpu_corpus 压缩后只有 896 MiB,往返全都落在系统内存里,几乎不碰硬盘。倍率以 1 个核心为基准。
| 核心数 | 吞吐 (MiB/s) | 倍率 | 多核用上了几成 |
|---|---|---|---|
| 1 | 477.1 | 1.00 | - |
| 2 | 905.3 | 1.90 | 0.95 |
| 4 | 1364.9 | 2.86 | 0.72 |
| 8 | 2394.1 | 5.02 | 0.63 |
| 16 | 2863.3 | 6.00 | 0.38 |
计算侧上限为 6.00 倍。8 个核心到 16 个核心只多 1.20 倍,因为后 8 个是超线程,不是真核心。
换成 NCIEP 这样的真实数据,倍率大幅缩水:
| 核心数 | 压缩 (MiB/s) | 压缩倍率 | 解压 (MiB/s) | 解压倍率 |
|---|---|---|---|---|
| 1 | 203.6 | 1.00 | 169.4 | 1.00 |
| 8 | 309.4 | 1.52 | 241.3 | 1.42 |
| 16 | 304.1 | 1.49 | 306.1 | 1.81 |
单元格里的值是重复测量的平均值,1 个核心与 16 个核心各测了两次。
压缩只到 1.49 倍,解压 1.81 倍,远达不到计算侧的 6.00 倍。而且 8 个核心和 16 个核心基本一样,说明再加核心已经没有用了。
3.2 卡在哪
两个对照实验用来分清到底是计算慢还是读写慢:
| 实验 | 说明 | 1 个核心 (MiB/s) | 16 个核心 (MiB/s) | 倍率 |
|---|---|---|---|---|
| 16 个大文件 | 13,312 MiB 随机数据,不可压缩,其余流程相同 | 333.8 | 453.4 | 1.36 |
| 真实 45,390 个文件 | 用 pack 模式,即完全不压缩也不加密 | 290.6 | 306.9 | 1.06 |
第二条是关键:完全不压缩、不加密,16 个核心的吞吐是 306.9 MiB/s,而正常压缩加密是 306.7 MiB/s,两者一模一样。计算时间被完全藏在了读写后面,一点都没体现出来。
硬盘单独跑有多快:
| 操作 | 吞吐 (MiB/s) | 吞吐 (文件/秒) |
|---|---|---|
| 顺序写 8 GiB 到一个文件 | 2880 | - |
| 顺序读 8 GiB 一个文件 | 6775 | - |
| 单线程读完这 45,390 个文件 | 2100 | 6,877 |
| 只列目录和取文件信息,不读内容 | 只受文件数限制 | 37,825 |
硬盘单独读同一份数据能到 2,100 MiB/s,而程序这条路径只跑到约 306 MiB/s,差约 7 倍。
文件个数的影响也很直接:同样是 13 GiB 左右的数据,分成 16 个大文件时吞吐是 453.4 MiB/s,分成 45,390 个小文件时掉到 306.7 MiB/s,少了三成。
结论是卡在读和写这一条线,不是压缩算法。压缩算法再怎么优化也不会让程序更快。
3.3 还没做到的
程序目前不输出读、算、写各自的分段吞吐,所以没法确定这 306 MiB/s 里各部分各占多少。上面 2,100 与 306 的 7 倍差距是两处独立测量的对比,不是同一份代码内部的分段结果;文件个数的影响也是推算,不是直接量出来的。
4. 版本演进实测
4.1 主表
倍率以 v2.1.0 为基准。
| 版本 | 压缩 (MiB/s) | 压缩倍率 | 解压 (MiB/s) | 解压倍率 | 归档体积 (MiB) |
|---|---|---|---|---|---|
| v2.1.0 | 26.0 | 1.00 | 32.8 | 1.00 | 13,441 |
| v2.2.0 | 134.2 | 5.16 | 134.3 | 4.09 | 13,441 |
| v2.2.1 | 254.3 | 9.78 | 325.1 | 9.91 | 13,430 |
4.2 每一步各自贡献了多少
倍率本身没有单位。
| 版本变化 | 压缩倍率 | 解压倍率 |
|---|---|---|
| v2.1.0 到 v2.2.0 | 5.16 | 4.09 |
| v2.2.0 到 v2.2.1 | 1.89 | 2.42 |
| v2.1.0 到 v2.2.1,合计 | 9.78 | 9.91 |
两段的含义不同:
- v2.1.0 到 v2.2.0:v2.1.0 是流水线之前的串行版本。v2.2.0 把压缩和解压都改成了专职读线程、N 个计算工人、专职写线程的三段流水线,所以两边都变快。
- v2.2.0 到 v2.2.1:结构没再动,改的是算法本身,即 Huffman 编解码重写、AES 换成标准实现走硬件、开启 LTO,见 4.3 到 4.5 节。
主表最后一行的倍率同时回答了另一个问题:当前版本比起单线程版本,压缩吞吐是 9.78 倍,解压吞吐是 9.91 倍。
4.3 压缩与加密内核单独测
| 项目 | 单位 | v2.1.0 与 v2.2.0 | v2.2.1 | 倍率 |
|---|---|---|---|---|
| 压缩(Huffman 编码) | MiB/s | 63.2 | 471.1 | 7.46 |
| 解压(Huffman 解码) | MiB/s | 91.3 | 236.8 | 2.59 |
| 加密 | MiB/s | 62.8 | 1261.5 | 20.1 |
| 解密 | MiB/s | 62.1 | 1279.6 | 20.6 |
| 每个数据块的说明信息 | 字节 | 约 1023 | 258 | 0.25 |
最后一行是字节数,倍率 0.25 表示降到原来的四分之一。压缩内核的倍率是 7.46,但 4.2 节里 v2.2.0 到 v2.2.1 的实际倍率只有 1.89。差距的原因就是第 1 节说的那件事:实际跑的时候,压缩已经快过读写了,再快也体现不出来。
4.4 压缩内核是分几步改的
编码侧每一步单独看:
| 改动 | 吞吐 (MiB/s) | 倍率 |
|---|---|---|
| 最初 | 59.7 | 1.00 |
| 把符号表从哈希表换成数组 | 111.1 | 1.86 |
| 再把逐位写入换成 64 位缓冲 | 156.8 | 2.63 |
| 再加链接时优化(LTO) | 827.8 | 13.87 |
此表用的是 8 MB 均匀数据,比真实数据更容易压缩,所以数字比 4.3 节高,两组不能直接比。
解码侧:
| 改动 | 吞吐 (MiB/s) | 倍率 |
|---|---|---|
| v2.2.0 | 91.3 | 1.00 |
| 前述两项改动,只影响编码,解码不变 | 92.1 | 1.01 |
| 加链接时优化 | 114.5 | 1.25 |
| 换成 canonical 码加 12 位查表 | 236.8 | 2.59 |
解码表的倍率以 v2.2.0 为基准。
4.5 加密为什么快了 20 倍
v2.2.0 用的是自己写的 AES-CFB,纯软件计算,加密 62.8、解密 62.1 MiB/s。v2.2.1 改用 Windows 系统自带的标准 AES-128-CTR,直接走 CPU 的 AES-NI 硬件指令。选型时实测了系统提供的几种模式:
| 模式 | 吞吐 (MiB/s) | 相对自研实现倍率 | 结论 |
|---|---|---|---|
| ECB 批量,用来生成密钥流 | 9005 | 143 | 采用,最快 |
| GCM | 6457 | 103 | 需要 28 字节前缀,与现有 16 字节 IV 布局冲突,放弃 |
| 自带 CFB | 加密 54.4,解密 70.5 | 0.87 与 1.13 | 非标准且不走硬件,不可用 |
| 自带 CTR | 不支持 | - | 返回 STATUS_INVALID_PARAMETER |
倍率以自研 AES-CFB 的 62.8 与 62.1 为基准。
5. 已知问题与边界
| 项目 | 数据 | 说明 |
|---|---|---|
| 极端偏斜数据解压 | 288.5 降到 215.8 MiB/s,倍率 0.75 | 12 位定长查表每个符号的成本高于原来的两步走树 |
6. 数字的局限
- 内核数字与整体数字不能互相换算。以压缩为例,内核倍率 7.46,实际只有 1.89。
- 吞吐量非常依赖测什么数据:容易压缩的数据与真实数据在 3.1 节内部就差了 6 倍以上,2863.3 MiB/s 对 306.1 MiB/s。
- 4.1 节三个版本不是同一批跑的:v2.1.0 与 v2.2.1 一批,v2.2.0 与 v2.2.1 另一批。v2.2.1 在两批里分别是压缩 275.4 与 254.3、解压 345.8 与 325.1 MiB/s,批间差约 8% 与 6%。这个量级远小于版本间的差距,最小的一段也有 1.89 倍,不影响结论。
- 压缩方向重复测很稳,两次相差不到 2%;解压方向不稳,16 个核心两次测出 275.6 与 336.6 MiB/s,差约 20%,因为解压要重新生成 45,390 个文件。3.1 节解压的 1.81 倍只能当大概量级看。
部分信息可能已经过时
粤公网安备44011102484817号