如何高效将多维NumPy数组分写入多文件?(附多进程尝试反馈)
批量保存NumPy图像数组的高效玩法
先搞懂为啥你之前的并行反而更慢
- 磁盘IO才是最大的坑:10000张750×750的图,单张按3通道uint8算就6.3MB,总数据量60GB往上。机械硬盘的随机写入本来就拉胯,多进程/线程同时写会让磁盘磁头来回跳,反而拖慢速度;SSD虽然好点,但并发数太高也会触发IO瓶颈。
- 进程池的额外开销:用
concurrent.futures.ProcessPoolExecutor时,每个子进程都要复制你要存的图像切片,大数组的拷贝成本比并行省的时间还多,当然越跑越慢。
靠谱的优化方案
1. 先把单线程的效率拉满
- 选对存储格式:别用PIL存PNG/JPG(压缩太耗时间),如果不需要直接看图,用
numpy.save存npy格式最快;要是必须存可视化格式,就存未压缩的BMP,比PNG快好几倍。
代码示例:import numpy as np from PIL import Image # 假设arr是你的(i, 750, 750, z)数组 arr = np.random.rand(10000, 750, 750, 3).astype(np.uint8) # 最快的方式:存npy格式 for idx in range(arr.shape[0]): np.save(f"img_{idx}.npy", arr[idx]) # 要可视化的话存未压缩BMP for idx in range(arr.shape[0]): img = Image.fromarray(arr[idx]) img.save(f"img_{idx}.bmp", format="BMP") - 先写大文件再拆分:把整个数组先一次性写入一个二进制文件,然后用系统的
split工具拆分,避开Python循环的IO开销,适合不需要实时处理的场景。
2. 非要并行?控制并发数+避免数据拷贝
- 用共享内存存大数组:把原始数组放到共享内存里,子进程直接读,不用每次拷贝切片,这能省掉一大笔开销。同时把并发数控制在2-4个(机械硬盘就2个,SSD可以到4个),别让磁盘忙不过来。
代码示例:import numpy as np from multiprocessing import Pool, Array from PIL import Image def save_single(args): idx, shared_buf, x, y, z = args # 从共享内存里取出对应图像 img_arr = np.frombuffer(shared_buf, dtype=np.uint8).reshape((-1, x, y, z))[idx] img = Image.fromarray(img_arr) img.save(f"img_{idx}.bmp") if __name__ == "__main__": arr = np.random.rand(10000, 750, 750, 3).astype(np.uint8) x, y, z = arr.shape[1:] # 创建共享内存缓冲区 shared_buf = Array('B', arr.size) # 把数据拷贝到共享内存 np.copyto(np.frombuffer(shared_buf.get_obj(), dtype=np.uint8), arr.flatten()) # 控制进程数为4 with Pool(processes=4) as pool: pool.map(save_single, [(i, shared_buf, x, y, z) for i in range(arr.shape[0])]) - 用线程池代替进程池:线程共享内存,没有数据拷贝的问题,同样控制并发数,开销比进程池小很多。
3. 用专门的工具库省心
- Dask:自动帮你分块、调度并行任务,不用自己管并发数,能避免IO过载。
代码示例:import dask.array as da import numpy as np from PIL import Image arr = np.random.rand(10000, 750, 750, 3).astype(np.uint8) # 按100张图为一块分块 dask_arr = da.from_array(arr, chunks=(100, 750, 750, 3)) def save_chunk(chunk): for idx, img_arr in enumerate(chunk): img = Image.fromarray(img_arr) img.save(f"img_{idx}.bmp") # 执行并行保存 dask_arr.map_blocks(save_chunk, dtype=da.void()).compute() - Zarr:适合存分块数组,后续要读取单个图像也方便,批量导出也很顺手。
额外小技巧
- 机械硬盘的话,把文件写到连续分区,减少磁头来回跑的时间。
- 提前建好输出目录,别让程序每次存文件时都创建目录。
- 能降精度就降:比如把float32转成uint8,文件大小直接砍到1/4,写入速度自然快。
内容的提问来源于stack exchange,提问作者alwayscurious
相关产品推荐
相关产品推荐

