C# 批量图像缩放与归档:如何优化 I/O 和并行处理性能?
可以让 JPEG 编码器直接写入 ZIP 条目流,省去逐张保存中间 JPEG 文件、再由归档程序读取的步骤。整个 ZIP 无需保存在内存中,可以直接写入最终归档文件。对于当前场景,建议先验证这条路径,并单独测量归档压缩和网络传输的耗时,再决定是否更换图像库。
现有数据还不能证明磁盘是唯一瓶颈,也不能确认 HALCON 存在线程冲突。归档压缩、JPEG 编码、网络写入和并发调度都需要分别测量。
先统一测试口径
表中部分阶段的耗时之和大于 Total Time。如果阶段耗时是各线程逐张计时后的累加值,而总时间是整批任务的墙钟时间,这种情况是可能的。但需要明确计时方式,不能直接把累加值当作整批任务的独占耗时。
建议分别记录:
- 整批任务的墙钟时间,以及是否包含约 150 秒的归档阶段。
- 读取、解码、缩放、JPEG 编码、归档和网络复制各自的耗时。
- 输入、JPEG 输出和最终归档的总字节数,以及 CPU 利用率、磁盘吞吐量和峰值内存。
原代码中的 image.Save(...) 和 WriteImage(...) 都涉及编码和写入,因此 Write 耗时较高不能直接证明磁盘慢。可以将相同输出分别编码到 MemoryStream 和文件中,比较两者耗时,初步区分编码成本和文件写入成本。
当前三种缩放设置分别是 NearestNeighbor、Box 和 bilinear,输出质量并不相同。Qual 表示插值设置,JpegEncoder.Quality = 70 表示 JPEG 编码质量,需要分开比较。不同库中同为 70 的 JPEG 质量值,也不保证输出大小和视觉质量一致。
将 JPEG 直接写入 ZIP
ZipArchive.CreateEntry(...) 可以创建归档条目,再通过 entry.Open() 获取写入流。ImageSharp 支持向流编码图像,因此可以连接这两个接口。Microsoft 文档、ImageSharp 文档
下面的基线示例顺序执行,用于测量取消中间 JPEG 文件后的耗时。示例假定目标目录已存在、目标尺寸有效,且 ImageSharp API 与项目所用版本兼容;它不包含网络复制和并行调度。
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
public static class ImageBatchArchiver
{
public static void CreateZip(
IEnumerable<string> files,
string zipPath,
int newWidth,
int newHeight)
{
using var output = new FileStream(
zipPath, FileMode.CreateNew,
FileAccess.Write, FileShare.None);
using var archive = new ZipArchive(
output, ZipArchiveMode.Create);
int index = 0;
foreach (string file in files)
{
using var image = Image.Load(
new DecoderOptions { SkipMetadata = true }, file);
image.Mutate(x => x.Resize(
newWidth, newHeight, KnownResamplers.Box));
string entryName =
(index++).ToString("D6") + "-" +
Path.GetFileNameWithoutExtension(file) + ".jpg";
var entry = archive.CreateEntry(
entryName, CompressionLevel.NoCompression);
using var entryStream = entry.Open();
image.Save(entryStream, new JpegEncoder { Quality = 70 });
}
}
}
这里使用 CompressionLevel.NoCompression 建立“只打包”的对照。JPEG 已经经过压缩,再次压缩能节省多少空间,需要用实际图片测量。可以再与 Fastest 等设置比较,观察节省的网络传输时间是否足以抵消额外的 CPU 成本。
这个方案仍然需要读取原图、解码、编码并写入最终 ZIP,省去的是中间 JPEG 文件的写入和再次读取。文件系统缓存可能已经吸收了部分重复读取,因此不能只按文件大小推算实际收益。
还要区分工具与格式:使用 7-Zip 并不代表当前输出一定是 .7z。上述代码生成的是 ZIP。如果必须保留 .7z 格式,需要另行确认所用归档接口是否支持多个文件的流式输入,不能直接套用 ZIP 的条目接口。
保留并行处理时,用有界队列连接各阶段
顺序示例便于建立基线,但会串行执行读取、缩放和编码。需要进一步提高吞吐量时,可以采用以下流程:
输入文件
→ 少量处理 worker:读取、解码、缩放、JPEG 编码
→ 有界队列:条目名和单张 JPEG 缓冲区
→ 单个归档 writer:依次写入 ZIP
→ 完成归档后复制到网络驱动器
处理 worker 可以并行生成单张 JPEG 的内存缓冲区,归档 writer 则依次创建条目、复制缓冲区并及时释放。对同一个 ZipArchive,让一个 writer 负责写入,避免多个 worker 同时操作归档;创建模式下,条目流也应依次打开和关闭。.NET 实现参考
队列必须有容量限制。归档或网络写入变慢时,上游应等待,避免缓冲区持续堆积。内存预算还需要包含解码后的图像和编码临时数据,不能只根据原文件的 300~400 KB 估算。如果图片尺寸差异大,最好同时限制在途数据的总字节数。
也可以直接向网络文件写入 ZIP,省去本地归档后的复制,但网络延迟和断连会直接影响整个流水线。应分别测量“本地生成后复制”和“直接网络写入”,再根据吞吐量与失败恢复要求选择。归档释放并完成收尾前,不应将目标文件标记为处理成功。
HALCON:分别测试外部并行与内部并行
先选择一个主要的并行层,再逐步增加另一层的并发,比较起来更容易定位问题。
| 测试方案 | C# 图像处理并发 | HALCON AOP | 观察重点 |
|---|---|---|---|
| 外部并行 | 多个 worker | 关闭或限制为 1 | 多张图像同时处理的吞吐量 |
| 内部并行 | 单个处理 worker | 开启 | 单张图像操作的并行收益 |
| 混合并行 | 少量 worker | 少量内部线程 | 增加并发是否带来收益或资源争用 |
MVTec 文档说明,parallelize_operators 可以关闭自动算子并行,thread_num = 1 也会关闭 AOP。关闭 AOP 本身不会关闭可重入支持。HALCON set_system 文档
例如,测试外部并行方案时,可以在启动 worker 前配置:
HOperatorSet.SetSystem("parallelize_operators", "false");
随后由受限数量的 C# worker 处理不同图像,每次处理分别创建和释放自己的 HImage。不要为了降低开销而在多线程场景中设置 reentrant=false。
如果保留 AOP,可以在单个 HALCON 处理 worker 前后安排独立的读取和输出阶段。C# 的顺序 foreach 不会自动实现 I/O 重叠,但图像处理阶段顺序执行,并不妨碍整个服务采用流水线。
tsp_thread_num 是线程专用设置,应在实际执行 HALCON 调用的线程上配置。如果使用 async/await,续体可能切换线程,不能把它当作随异步任务移动的配置。应按已安装 HALCON 版本的文档确认行为,并避免在热循环中反复修改全局设置。HALCON 参数说明
替代库值得测,但不能预先认定更快
可以把 NetVips 加入候选。它是 libvips 的 .NET 绑定;libvips 使用按需执行和内部线程机制,值得针对批量缩放、JPEG 输出进行对照测试。这些设计特点不能直接换算成当前服务的加速比例。NetVips 项目、libvips 官方介绍
比较时固定输入图片、目标尺寸、可接受画质、元数据处理方式和归档设置,分别测量纯图像处理与端到端耗时。也要计入本机依赖的部署成本。
当前可以先比较相同 JPEG 数据在“不压缩打包、压缩打包、本地输出、网络输出”几种情况下的耗时,再测试取消中间 JPEG 文件。这样有助于判断约 150 秒主要花在压缩、文件操作还是网络传输上,并据此安排后续优化。
备注:内容仅供参考。
