Acl.Excel 跨库基准:读取最高快 32.8 倍
Acl.Excel 跨库基准:读取最高快 32.8 倍
本文力求客观:既呈现 Acl.Excel 的实测数据,也如实标注测试口径、版本差异与已知局限,供读者自行判断。
口径说明:本机实测(单一机器、单轮次),运行于 .NET 8/10 Release,使用 BenchmarkDotNet 0.15.8 测量;Acl.Excel 4.0.0 为基线,对照 MiniExcel 1.45.0、ClosedXML 0.104.0、NPOI 2.7.2 等(竞品版本以正文各表为准);百万行级对等测试数据来自 MiniExcel 官方 benchmark 仓库。已知局限:未做跨机器复现与长期统计。
Excel系列文章 第10篇 / 共11篇
系列导航:← 上一篇:安全设计 · 下一篇:实战指南 →
本系列第七篇呈现 Acl.Excel 的完整性能基准测试数据,包括与 MiniExcel、ClosedXML、NPOI 的跨库对比,以及压缩引擎、字节扫描器、写入引擎各环节的独立基准数据。所有测试基于 BenchmarkDotNet 实测,可复现。
读取同一份 10,000 行 x 7 列的 xlsx,ClosedXML 用了 355.36 ms,Acl.Excel 用了 10.84 ms,快 32.8 倍。更值得注意的是内存:写入路径上 Acl.Excel 只分配了 462 KB,而 ClosedXML 分配了 165,895 KB,相差 359 倍。在一个每秒要导出几十份报表的 API 服务里,这个差距不只是"快一点",而是 GC 会不会成为瓶颈的分水岭。
本文把 Acl.Excel 的性能画像完整摊开:从跨库横评、百万行级对等测试,到写入引擎、压缩器、解压器、字节扫描器四个微观组件的独立基准。读完你会知道每一个数字是怎么来的,以及在你自己的场景里该选哪套策略。所有测试在 .NET 8/10 Release 配置下运行,使用 BenchmarkDotNet 0.15+ 进行测量。
核心结论
- 10K 行 x 7 列场景,写入比 MiniExcel 快约 2 倍、比 ClosedXML/NPOI 快一个数量级;读取比 MiniExcel 快 5.6 倍、比 ClosedXML 快 32.8 倍。
- 内存分配是更大的差距:写入分配仅为 MiniExcel 的 1/94、ClosedXML 的 1/359,Gen0 回收次数为 0。
- 百万行级对等测试(SlidingWindow 4MB)中,全量读取 1.88x、写入 2.84x 快于 MiniExcel,分配分别降至 1/5.8 与 1/37.5。
- 纯托管的 LibDeflate 解压在典型 Excel 体量(100KB-10MB)稳定拿到 1.8x-2.7x,代价是零 P/Invoke、零原生依赖。
- 压缩档位可按场景取舍:L1 压缩比 3.15 换 2.1x 速度,L9 压缩比 4.05 但速度降到 0.6x。
一、跨库对比:同一份数据,四个库各跑一遍
1.1 先说清楚测试怎么跑的
性能数字脱离测试条件就没有意义,先把环境交代清楚。
| 参数 | 值 |
|---|---|
| 运行时 | .NET 8.0 / .NET 10.0 |
| 配置 | Release |
| 基准工具 | BenchmarkDotNet 0.15.8 |
| 数据规模 | 10,000 行 x 7 列 |
| 数据类型 | 混合(字符串、整型、浮点、日期) |
数据类型刻意选了混合型,而不是清一色的字符串或数值:后者容易让某些库的特化路径吃到不该吃的红利,与真实业务表格的形态也不符。
1.2 写入性能:13.35 ms 与 462 KB
| 库 | 版本 | Mean | Allocated | Gen0 |
|---|---|---|---|---|
| Acl.Excel | 4.0.0 | 13.35 ms | 462 KB | — |
| MiniExcel | 1.45.0 | 27.49 ms | 43,498 KB | 1700 |
| ClosedXML | 0.104.0 | 173.27 ms | 165,895 KB | 3000 |
| NPOI | 2.7.2 | 184.72 ms | 108,165 KB | 2000 |
耗时上的领先是可预期的,真正拉开身位的是 Allocated 那一列。
Acl.Excel 在写入路径上的分配仅为 MiniExcel 的 1/94,ClosedXML 的 1/359。这意味着在 GC 压力敏感的场景(如高频 API 服务)中,Acl.Excel 的 GC 暂停时间远低于其他库。Gen0 一列的 "—" 也值得留意:整个基准跑完没有触发 Gen0 回收,而其他三个库的 Gen0 次数在 1700~3000 之间。
1.3 读取性能:差距被进一步拉开
| 库 | 版本 | Mean | Allocated | Gen0 |
|---|---|---|---|---|
| Acl.Excel | 4.0.0 | 10.84 ms | 2,350 KB | — |
| MiniExcel | 1.45.0 | 60.49 ms | 57,788 KB | 1900 |
| NPOI | 2.7.2 | 250.91 ms | 111,192 KB | 2800 |
| ClosedXML | 0.104.0 | 355.36 ms | 254,542 KB | 5000 |
读取路径的差距比写入更大:Acl.Excel 比 MiniExcel 快 5.6 倍,比 ClosedXML 快 32.8 倍。这主要归功于字节扫描器(绕过 XmlReader)和 LibDeflate 解压(绕过 DeflateStream)的双重优化。
换句话说,读取之所以领先更多,不是因为多做了什么,而是因为少做了两层:不必把字节流先解成字符流再交给 XmlReader,也不必让解压走 DeflateStream 的托管封装。
二、百万行级基准:内存恒定条件下的对等较量
10K 行只能说明常规业务表格的表现。真正考验架构的是百万行级,那也是最容易"跑得快但内存爆掉"的量级。
在 100 万行 x 10 列的基准测试中(使用 MiniExcel 官方仓库的测试数据):
| 操作 | 库 | Mean | Allocated |
|---|---|---|---|
| Query(全量读取) | Acl.Excel | 1.88x 快于 MiniExcel | 1/5.8 |
| QueryFirst(首行) | Acl.Excel | 81µs vs 85µs(持平) | — |
| Create(写入) | Acl.Excel | 2.84x 快于 MiniExcel | 1/37.5 |
Acl.Excel 使用 SlidingWindow 策略(4MB 滑动窗口),与 MiniExcel 的流式读取条件对等对比。QueryFirst 仅取首行,Acl.Excel 和 MiniExcel 在微秒级几乎持平,说明两者的首行读取开销接近。
这里的公平性值得强调一句:SlidingWindow 并不是 Acl.Excel 最快的读取策略,但它是唯一能与 MiniExcel 流式读取形成对等条件的策略。用更慢的那一档去比,结论才站得住。
三、拆开看写入引擎:Utf8RawWriter 省在哪
3.1 Utf8RawWriter vs StreamWriter + XmlWriter
| 数据规模 | 端到端 | Utf8RawWriter | 旧 XmlWriter | 分配降幅 |
|---|---|---|---|---|
| 10K x 7 | 39ms | 38.66ms | 39.23ms | 63% |
| 100K x 7(数值) | 202ms | 110ms | 173ms | — |
| 100K x 7(文本) | 197ms | 123ms | 254ms | — |
| 100K x 7(混合) | 117ms | 57ms | 180ms | — |
数据量越大,Utf8RawWriter 的优势越明显。文本为主的场景端到端耗时降幅最大(-51.7%),因为 XmlWriter 在文本场景下字符串分配开销最高。
10K 规模下两者耗时几乎一样,但分配已经降了 63%,这说明小数据量时瓶颈不在写入器本身;等规模上到 100K,省下来的分配才开始转化为可见的时间收益。
3.2 压缩策略对比
写入速度和文件大小是一对此消彼长的取舍,Acl.Excel 把选择权交给调用方:
| 策略 | 写入速度 | 文件大小 | 适用场景 |
|---|---|---|---|
| Throughput(.NET 内置) | 基准 | 基准 | 吞吐优先 |
| Balanced(libdeflate L1) | ~2x | +14% | 性价比最优 |
| Compression(libdeflate L12) | 较慢 | 最小 | 归档存储 |
多数业务场景选 Balanced 就够了:用 14% 的体积换 2 倍写入速度,在网络传输和磁盘都不紧张的情况下是划算的。
四、解压引擎:纯托管能追到原生的几成
| 引擎 | 1KB | 100KB | 1MB | 10MB | 100MB |
|---|---|---|---|---|---|
| .NET DeflateStream | 1x | 1x | 1x | 1x | 1x |
| LibDeflateDecompressor | 0.8x | 1.8x | 2.1x | 2.3x | 2.7x |
| zlib-ng (原生) | 2.5x | 3.2x | 3.5x | 3.8x | 4.0x |
小块数据(<4KB)场景下,Acl.Excel 的解压器略慢于 .NET,因为 .NET 的调用开销更低。在典型 Excel 场景(100KB-10MB),优势稳定在 2x 左右。与原生 zlib-ng 相比仍有 2.4x 差距,但 Acl.Excel 的优势在于零 P/Invoke。
这张表其实回答了一个常被问到的问题:既然原生更快,为什么不直接 P/Invoke?答案是部署成本:零原生依赖意味着不用为每个目标平台准备一份 .so/.dll,容器镜像不用额外装库,AOT 裁剪也不会踩坑。用一部分理论峰值换取"复制即可运行",对绝大多数服务端场景是更优解。
五、压缩器:压缩比与速度怎么取舍
| 引擎 | 压缩比 | 压缩速度 | 解压速度 |
|---|---|---|---|
| .NET DeflateStream Optimal | 3.82 | 基准 | 基准 |
| LibDeflateCompressor L1 | 3.15 | 2.1x | 1.9x |
| LibDeflateCompressor L6 | 3.91 | 0.9x | 1.9x |
| LibDeflateCompressor L9 | 4.05 | 0.6x | 1.9x |
L1 级别适合写入吞吐优先的场景,L6 在压缩比接近 .NET Optimal 的同时有更快的解压速度。L9 提供最高压缩比但压缩速度较慢,适合一次压缩多次读取的场景。
有个细节容易被忽略:三个档位的解压速度都是 1.9x,与压缩档位无关。也就是说,如果文件写一次、读很多次,用 L9 多花的压缩时间会被后续每一次读取的收益摊薄。
六、字节扫描器:三种策略的取舍
三扫描器在 100K 行 x 10 列场景下的对比:
| 扫描器 | Mean | Allocated | RSS 峰值 |
|---|---|---|---|
| ByteSheetScanner v1 (Fast) | 最快 | 中等 | 文件大小相关 |
| ByteSheetScanner2 v2 (SlidingWindow) | 快 | 最低 | 恒定 4MB |
| XmlReaderSheetScanner (Standard) | 慢 | 高 | 低 |
v2 的滑动窗口策略在超大文件场景下提供恒定的 RSS 峰值内存,是与 MiniExcel 公平对比时使用的策略。
选型逻辑很直接:文件体量可控就用 v1 换速度,文件体量不可控(用户上传、批量导入)就用 v2 保住内存上限。
七、支撑这些数字的测试体系
基准数据只有在"不会悄悄退化"的前提下才有长期价值。Acl.Excel 的测试体系包括:
- 单元测试:1500+ 用例,覆盖核心组件、压缩引擎、安全防护、边界条件
- Golden 验证:读取参考文件,验证输出与预期一致
- 跨库互操作测试:Acl.Excel 写入的文件被其他库读取,反之亦然
- 性能回归测试:每次代码变更后运行 BenchmarkDotNet 基准测试,性能回退时自动拦截
- 并发测试:多线程读写场景下的正确性验证
八、性能优化的工程纪律
Acl.Excel 的性能优化遵循严格的工程纪律:
- 修改前备份代码
- 修改前运行 cdnfull 基准测试(排除 NPOI、ClosedXML、MiniExcel 对比测试)
- 修改后运行单元测试
- 单元测试通过后运行性能测试
- 性能不满足要求时立即回退
这个纪律确保了每次优化都是可验证、可追溯的,避免了"优化后反而变慢"的常见陷阱。
九、关键收获
- 看分配比看耗时更重要:写入 462 KB 对 165,895 KB 的差距,决定的是服务在高并发下会不会被 GC 拖垮,而不只是单次调用快几毫秒。
- 读取的领先来自"少做两层":绕过 XmlReader 的字节扫描器,加上绕过 DeflateStream 的 LibDeflate 解压,共同贡献了 5.6 倍到 32.8 倍的差距。
- 对等条件下的百万行才是硬指标:用较慢的 SlidingWindow 策略去比,仍然拿到 1.88x 读取、2.84x 写入,同时把 RSS 峰值锁在恒定 4MB。
- 压缩档位按读写比例选:写多读少选 L1(3.15 压缩比、2.1x 速度),写少读多选 L9(4.05 压缩比),三档解压速度同为 1.9x。
- 零 P/Invoke 是一项主动取舍:相比原生 zlib-ng 留有差距,换来的是无原生依赖、跨平台一致、部署零负担。
下一篇是系列终篇:把本文这些基准数字翻译成可直接照抄的场景选型与调优清单,告诉你什么时候用 SlidingWindow、什么时候切 Balanced、大文件导出该怎么组织代码。
标签(建议):Acl.Excel, .NET, 性能优化, BenchmarkDotNet, Excel 处理
免责声明:本文基于 Acl.Excel 4.0.0(撰写日期前后实测)数据撰写,测试环境与口径见正文;数据可能随版本演进变化,重要决策请自行复测核验。
客观性说明:本文的结论基于以下事实约束,而非自诩无偏——① 本机实测(单一机器、单轮次),运行于 .NET 8/10 Release,使用 BenchmarkDotNet 0.15.8 测量;② 以 Acl.Excel 4.0.0 为基线,对照 MiniExcel 1.45.0、ClosedXML 0.104.0、NPOI 2.7.2 等(竞品版本以正文各表为准),百万行级对等测试数据来自 MiniExcel 官方 benchmark 仓库;③ 未做跨机器复现与长期统计,数据可能随版本演进变化。以上局限已在正文相应位置如实标注,供读者结合完整信息自行判断。
系列导航
Acl.Excel 系列文章(共 11 篇)
序号 文章 重点 ① 从 libdeflate 到纯 C#:Acl.Excel DEFLATE 解压/压缩器的移植与优化之旅 纯 C# 移植 libdeflate ② 从 libdeflate 到纯 C#(续):优化纯 C# DEFLATE 压缩器——从 1.6x 到 4.9x 的提速之路 14 轮优化提速 ③ Acl.Excel vs MiniExcel 1.45.0:性能对比与选型分析(.NET 8 基准) 百万行对等实测 ④ 不依赖 NPOI/ClosedXML,纯 C# Excel 库如何做到 30 倍性能? 概述与设计哲学 ⑤ 百万行Excel如何恒定内存?Acl.Excel流式拆解 读取引擎流式管线 ⑥ 24B消灭上亿次装箱:Acl.Excel零装箱设计 24B CellValue 零装箱 ⑦ 写入分配砍掉 63%:Acl.Excel 绕过 XmlWriter 字节直写 字节直写与并行写入 ⑧ 零依赖纯 C# DEFLATE 引擎:2916 行移植 libdeflate DEFLATE 引擎移植 ⑨ 一个 42KB 的 Excel 如何撑爆 4.5PB?Acl.Excel 8 层防御 安全纵深防御 ⑩ Acl.Excel 跨库基准:读取最高快 32.8 倍 跨库性能基准(本文) ⑪ 从实战到生产:Acl.Excel 八大场景与避坑指南(终篇) 实战与避坑指南 建议按 ①→⑪ 顺序阅读,从底层算法到实战落地形成完整认知。(当前本篇为第 10 篇,已以 粗体 标记。)

浙公网安备 33010602011771号