提升Apache Arrow读写性能 我最后选了一个更保守的办法不改 Arrow .NET 的默认实现基于它已有的压缩扩展点单独做一个可选库。也就是这个dotnet add package ArrowNet.Compression.NativeCompressions项目地址https://github.com/InCerryGit/ArrowNet.Compression.NativeCompressions这个库不是 Apache Arrow 官方包而是一个可选的高性能压缩后端。它通过 Apache Arrow .NET 暴露出来的ICompressionCodecFactory扩展点把底层压缩实现换成了 Cysharp 的 NativeCompressions。NativeCompressions 仓库地址https://github.com/Cysharp/NativeCompressions性能对比#先直接看结果。Benchmark 环境BenchmarkDotNet 0.15.8Ubuntu 24.04.2 LTSIntel Core i7-14700K.NET SDK 10.0.107Runtime .NET 8.0.26测试的是 Arrow IPC 读写路径不是单纯的 codec micro benchmark。也就是说写入路径里包含 Arrow IPC writer 和MemoryStream.ToArray()的成本。测试命令dotnet run --project benchmarks/ArrowNet.Compression.NativeCompressions.Benchmarks/ArrowNet.Compression.NativeCompressions.Benchmarks.csproj -c Release -f net8.0 -- --filter *ArrowIpcCompressionBenchmarks*测试数据是 deterministic 的int stringArrow RecordBatch分别测试10w 行50w 行100w 行对比对象Apache.Arrow.Compression.CompressionCodecFactoryNativeCompressionsCodecFactory结果如下RowsPathCodecApache meanApache allocatedNative meanNative allocatedTime differenceAllocated difference100kWrite compressed IPC streamLZ4 frame3.229 ms6,105.70 KB2.716 ms5,291.66 KB15.9% faster13.3% less100kRead compressed IPC streamLZ4 frame0.764 ms3.79 KB0.431 ms3.07 KB43.5% faster19.0% less100kWrite compressed IPC streamZstd4.205 ms2,762.03 KB3.318 ms3,064.87 KB21.1% faster11.0% more100kRead compressed IPC streamZstd1.555 ms3.12 KB1.313 ms3.16 KB15.6% faster1.3% more500kWrite compressed IPC streamLZ4 frame15.844 ms28,698.06 KB14.929 ms26,426.71 KB5.8% faster7.9% less500kRead compressed IPC streamLZ4 frame4.039 ms4.10 KB2.235 ms3.42 KB44.7% faster16.6% less500kWrite compressed IPC streamZstd21.681 ms13,536.49 KB17.133 ms15,023.90 KB21.0% faster11.0% more500kRead compressed IPC streamZstd8.181 ms3.45 KB6.800 ms3.48 KB16.9% faster0.9% more1MWrite compressed IPC streamLZ4 frame36.852 ms57,450.92 KB32.276 ms52,845.62 KB12.4% faster8.0% less1MRead compressed IPC streamLZ4 frame8.619 ms4.11 KB4.761 ms3.22 KB44.8% faster21.7% less1MWrite compressed IPC streamZstd41.588 ms27,016.95 KB36.714 ms29,987.13 KB11.7% faster11.0% more1MRead compressed IPC streamZstd16.717 ms3.74 KB14.523 ms4.14 KB13.1% faster10.7% more可以看到最明显的是 LZ4 read 场景。在 10w、50w、100w 三组数据下NativeCompressions 后端快了大约 44%managed allocation 也更低。Zstd 这边也有时间收益不过内存分配上并不是所有场景都更好。尤其是 Zstd write速度更快但 managed allocation 会多一些。所以这个优化不能简单理解成“所有场景都更好”。更准确地说LZ4 read收益非常明显时间和 managed allocation 都更好LZ4 write时间更快allocation 更少Zstd read/write时间更快但 allocation 可能略高。性能优化不能只看一个指标。只看耗时容易忽略 allocation只看 allocation又可能错过真实吞吐收益。这里的 allocated 是 BenchmarkDotNetMemoryDiagnoser统计出来的 managed allocation per operation不是进程峰值内存也不是 native memory。关于 NativeCompressions#NativeCompressions 是 Cysharp 做的 native compression binding / high-level API。它支持LZ4ZstandardOpenZL对于 Arrow .NET 来说最相关的就是CompressionCodecType.Lz4FrameCompressionCodecType.Zstd正好对应 Arrow IPC 当前公开的两个压缩 codec。不过要注意NativeCompressions 当前仍然是 preview 状态。它的 README 里也明确写了 API 可能变化不建议直接无脑用于所有生产环境。在这个库里它只负责替换 Arrow IPC 的 LZ4/Zstd codec 实现。Arrow 的数据结构、IPC 格式、reader/writer API 还是 Apache Arrow .NET 的。Arrow .NET 是怎么接入压缩的#Apache Arrow .NET 这里设计得比较好它没有把压缩实现完全写死。它提供了一个扩展点ICompressionCodecFactory也就是说只要实现这个 factory就可以让 Arrow reader / writer 使用自己的 codec。使用方式大概是这样using Apache.Arrow.Ipc; using ArrowNet.Compression.NativeCompressions; var options new IpcOptions { CompressionCodecFactory new NativeCompressionsCodecFactory(), CompressionCodec CompressionCodecType.Lz4Frame };如果使用 Zstd把CompressionCodec改成CompressionCodecType.Zstd即可。所以这个库可以做得很小。不需要 fork Apache Arrow也不需要改 Arrow 的源码只需要实现它已经暴露出来的 codec factory 即可。NativeCompressionsCodecFactory 做了什么#核心入口就是NativeCompressionsCodecFactory它负责根据 Arrow 的CompressionCodecType创建对应 codec。目前只支持两个CompressionCodecType.Lz4Frame CompressionCodecType.Zstd不支持的 codec 会直接抛NotSupportedException。这样做有一个好处失败是显式的。压缩格式这种东西最怕静默 fallback。你以为用了某个高性能 backend实际却 fallback 到别的实现这种问题很难排查。所以这里宁可直接失败也不要偷偷降级。LZ4 和 Zstd 的实现思路#实现上分别有两个 internal codecNativeCompressionsLz4CompressionCodecNativeCompressionsZstdCompressionCodecLZ4 路径使用 NativeCompressions 的 LZ4 API。Zstd 路径使用 NativeCompressions 的 Zstandard API默认压缩级别是 3。更值得注意的是压缩路径没有使用 one-shotCompress(...)返回新byte[]的方式。一开始我也看过这个方向但这会引入额外的临时压缩数组。对于 Arrow IPC 写入来说本来就有 writer、buffer、stream、ToArray()等成本再多一个临时大数组会让 allocation 更难看。所以现在的实现使用了ArrayPoolbyte.Sharedspan-based output API最大压缩长度预估压缩完成后只写实际压缩长度这样做不是严格意义上的“零拷贝”但已经是比较接近当前接口约束下的 minimal-copy 路径。对于解压路径Arrow 会给出目标输出大小。codec 只需要把压缩 payload 解到 Arrow 期望的目标 buffer 里即可。这里还有一个细节Arrow IPC buffer 里可能存在 padding所以 decoder 不能简单假设输入长度就等于压缩帧的精确长度。实现需要遵守 Arrow 的 exact-output-size contract。Benchmark 是怎么设计的#Benchmark 不是只测 codec 本身而是测端到端 Arrow IPC 读写路径。主要有两个 benchmarkWriteCompressedIpcStream()