8

TLDR;在python gzipbz2lzma等可用的各种压缩算法中,哪种解压性能最好?

全面讨论:

Python 3 有各种用于压缩/解压缩数据的模块, 包括gzipbz2lzma. gzip并且bz2还可以设置不同的压缩级别。

如果我的目标是平衡文件大小(/压缩比)和解压缩速度(压缩速度不是问题),那么哪个是最佳选择?解压缩速度比文件大小更重要,但由于有问题的未压缩文件每个约为 600-800MB(32 位 RGB .png 图像文件),而且我有十几个,我确实想要一些压缩。

  • 我的用例是我从磁盘加载十几个图像,对它们进行一些处理(作为 numpy 数组),然后在我的程序中使用处理后的数组数据。

    • 图像永远不会改变,我只需要在每次运行程序时加载它们。
    • 处理所需的时间与加载(几秒钟)大致相同,因此我试图通过保存已处理的数据(使用pickle)来节省一些加载时间,而不是每次都加载原始的、未处理的图像。最初的测试很有希望——加载原始/未压缩的腌制数据不到一秒,而加载和处理原始图像需要 3 或 4 秒——但如上所述导致文件大小约为 600-800MB,而原始 png 图像是只有大约 5MB。所以我希望我可以通过以压缩格式存储挑选的数据来在加载时间和文件大小之间取得平衡。
  • 更新:情况实际上比我上面所说的要复杂一些。我的应用程序使用PySide2,所以我可以访问这些Qt库。

    • pillow如果我读取图像并使用( )转换为 numpy 数组PIL.Image,我实际上不需要进行任何处理,但将图像读入数组的总时间约为 4 秒。
    • 相反,如果我QImage用来读取图像,那么我必须对结果进行一些处理,以使其可用于我的程序的其余部分,因为QImage加载数据的方式是字节序的——基本上我必须交换位顺序和然后旋转每个“像素”,使 alpha 通道(显然是由 QImage 添加的)最后而不是第一个。整个过程大约需要 3.8 秒,比仅使用 PIL稍微快一点。
    • 如果我保存numpy未压缩的数组,那么我可以在 0.8 秒内重新加载它们,这是迄今为止最快的,但文件大小很大。
┌────────────┬────────────────────────┬───────────────┬─────────────┐
│ Python Ver │     Library/Method     │ Read/unpack + │ Compression │
│            │                        │ Decompress (s)│    Ratio    │
├────────────┼────────────────────────┼───────────────┼─────────────┤
│ 3.7.2      │ pillow (PIL.Image)     │ 4.0           │ ~0.006      │
│ 3.7.2      │ Qt (QImage)            │ 3.8           │ ~0.006      │
│ 3.7.2      │ numpy (uncompressed)   │ 0.8           │ 1.0         │
│ 3.7.2      │ gzip (compresslevel=9) │ ?             │ ?           │
│ 3.7.2      │ gzip (compresslevel=?) │ ?             │ ?           │
│ 3.7.2      │ bz2 (compresslevel=9)  │ ?             │ ?           │
│ 3.7.2      │ bz2 (compresslevel=?)  │ ?             │ ?           │
│ 3.7.2      │ lzma                   │ ?             │ ?           │
├────────────┼────────────────────────┼───────────────┼─────────────┤
│ 3.7.3      │ ?                      │ ?             │ ?           │  
├────────────┼────────────────────────┼───────────────┼─────────────┤
│ 3.8beta1   │ ?                      │ ?             │ ?           │
├────────────┼────────────────────────┼───────────────┼─────────────┤
│ 3.8.0final │ ?                      │ ?             │ ?           │
├────────────┼────────────────────────┼───────────────┼─────────────┤
│ 3.5.7      │ ?                      │ ?             │ ?           │
├────────────┼────────────────────────┼───────────────┼─────────────┤
│ 3.6.10     │ ?                      │ ?             │ ?           │
└────────────┴────────────────────────┴───────────────┴─────────────┘

.png 图像示例:以这张 5.0Mb 的 png 图像为例,它是阿拉斯加海岸线的高分辨率图像

png/PIL 案例的代码(加载到numpy数组中):

from PIL import Image
import time
import numpy

start = time.time()
FILE = '/path/to/file/AlaskaCoast.png'
Image.MAX_IMAGE_PIXELS = None
img = Image.open(FILE)
arr = numpy.array(img)
print("Loaded in", time.time()-start)

在我使用 Python 3.7.2 的机器上,这个负载大约需要 4.2 秒。

或者,我可以加载通过选择上面创建的数组生成的未压缩的 pickle 文件。

未压缩的pickle负载情况的代码:

import pickle
import time

start = time.time()    
with open('/tmp/test_file.pickle','rb') as picklefile:
  arr = pickle.load(picklefile)    
print("Loaded in", time.time()-start)

在我的机器上从这个未压缩的 pickle 文件加载大约需要 0.8 秒。

4

4 回答 4

7

您可以使用 Python-blosc

非常快,对于小型阵列(<2GB)也非常易于使用。在像您的示例这样易于压缩的数据上,压缩数据以进行 IO 操作通常更快。(SATA-SSD:约 500 MB/s,PCIe-SSD:高达 3500MB/s) 在解压步骤中,阵列分配是最昂贵的部分。如果您的图像具有相似的形状,则可以避免重复的内存分配。

例子

以下示例假定一个连续数组。

import blosc
import pickle

def compress(arr,Path):
    #c = blosc.compress_ptr(arr.__array_interface__['data'][0], arr.size, arr.dtype.itemsize, clevel=3,cname='lz4',shuffle=blosc.SHUFFLE)
    c = blosc.compress_ptr(arr.__array_interface__['data'][0], arr.size, arr.dtype.itemsize, clevel=3,cname='zstd',shuffle=blosc.SHUFFLE)
    f=open(Path,"wb")
    pickle.dump((arr.shape, arr.dtype),f)
    f.write(c)
    f.close()
    return c,arr.shape, arr.dtype

def decompress(Path):
    f=open(Path,"rb")
    shape,dtype=pickle.load(f)
    c=f.read()
    #array allocation takes most of the time
    arr=np.empty(shape,dtype)
    blosc.decompress_ptr(c, arr.__array_interface__['data'][0])
    return arr

#Pass a preallocated array if you have many similar images
def decompress_pre(Path,arr):
    f=open(Path,"rb")
    shape,dtype=pickle.load(f)
    c=f.read()
    #array allocation takes most of the time
    blosc.decompress_ptr(c, arr.__array_interface__['data'][0])
    return arr

基准

#blosc.SHUFFLE, cname='zstd' -> 4728KB,  
%timeit compress(arr,"Test.dat")
1.03 s ± 12.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
#611 MB/s
%timeit decompress("Test.dat")
146 ms ± 481 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
#4310 MB/s
%timeit decompress_pre("Test.dat",arr)
50.9 ms ± 438 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
#12362 MB/s

#blosc.SHUFFLE, cname='lz4' -> 9118KB, 
%timeit compress(arr,"Test.dat")
32.1 ms ± 437 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
#19602 MB/s
%timeit decompress("Test.dat")
146 ms ± 332 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
#4310 MB/s
%timeit decompress_pre("Test.dat",arr)
53.6 ms ± 82.9 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
#11740 MB/s

编辑

这个版本更适合一般用途。它确实可以处理 f-contiguous、c-contiguous 和 non-contiguous 数组以及 >2GB 的数组。也看看bloscpack

import blosc
import pickle

def compress(file, arr,clevel=3,cname='lz4',shuffle=1):
    """
    file           path to file
    arr            numpy nd-array
    clevel         0..9
    cname          blosclz,lz4,lz4hc,snappy,zlib
    shuffle        0-> no shuffle, 1->shuffle,2->bitshuffle
    """
    max_blk_size=100_000_000 #100 MB 

    shape=arr.shape
    #dtype np.object is not implemented
    if arr.dtype==np.object:
        raise(TypeError("dtype np.object is not implemented"))

    #Handling of fortran ordered arrays (avoid copy)
    is_f_contiguous=False
    if arr.flags['F_CONTIGUOUS']==True:
        is_f_contiguous=True
        arr=arr.T.reshape(-1)
    else:
        arr=np.ascontiguousarray(arr.reshape(-1))

    #Writing
    max_num=max_blk_size//arr.dtype.itemsize
    num_chunks=arr.size//max_num

    if arr.size%max_num!=0:
        num_chunks+=1

    f=open(file,"wb")
    pickle.dump((shape,arr.size,arr.dtype,is_f_contiguous,num_chunks,max_num),f)
    size=np.empty(1,np.uint32)
    num_write=max_num
    for i in range(num_chunks):
        if max_num*(i+1)>arr.size:
            num_write=arr.size-max_num*i
        c = blosc.compress_ptr(arr[max_num*i:].__array_interface__['data'][0], num_write, 
                               arr.dtype.itemsize, clevel=clevel,cname=cname,shuffle=shuffle)
        size[0]=len(c)
        size.tofile(f)
        f.write(c)
    f.close()

def decompress(file,prealloc_arr=None):
    f=open(file,"rb")
    shape,arr_size,dtype,is_f_contiguous,num_chunks,max_num=pickle.load(f)

    if prealloc_arr is None:
        if prealloc_arr.flags['F_CONTIGUOUS']==True
            prealloc_arr=prealloc_arr.T
        if prealloc_arr.flags['C_CONTIGUOUS']!=True
            raise(TypeError("Contiguous array is needed"))
        arr=np.empty(arr_size,dtype)
    else:
        arr=np.frombuffer(prealloc_arr.data, dtype=dtype, count=arr_size)

    for i in range(num_chunks):
        size=np.fromfile(f,np.uint32,count=1)
        c=f.read(size[0])
        blosc.decompress_ptr(c, arr[max_num*i:].__array_interface__['data'][0])
    f.close()

    #reshape
    if is_f_contiguous:
        arr=arr.reshape(shape[::-1]).T
    else:
        arr=arr.reshape(shape)
    return arr
于 2019-06-25T19:47:53.460 回答
6

低垂的果实

numpy.savez_compressed('AlaskaCoast.npz', arr)
arr = numpy.load('AlaskaCoast.npz')['arr_0']

加载速度比基于 PIL 的代码快 2.3 倍。

它使用zipfile.ZIP_DEFLATED,请参阅savez_compressed文档。

您的 PIL 代码也有一个不需要的副本:array(img)应该是asarray(img). 它只花费慢加载时间的 5%。但是在优化之后,这将很重要,您必须记住哪些 numpy 运算符会创建副本。

快速减压

根据zstd benchmarks,优化解压时lz4是一个不错的选择。只需将其插入泡菜中即可获得 2.4 倍的增益,并且仅比未压缩的酸洗慢 30%。

import pickle
import lz4.frame

# with lz4.frame.open('AlaskaCoast.lz4', 'wb') as f:
#     pickle.dump(arr, f)

with lz4.frame.open('AlaskaCoast.lz4', 'rb') as f:
    arr = pickle.load(f)

基准

method                 size   load time
------                 ----   ---------
original (PNG+PIL)     5.1M   7.1
np.load (compressed)   6.7M   3.1
pickle + lz4           7.1M   1.3
pickle (uncompressed)  601M   1.0 (baseline)

加载时间是在 Python (3.7.3) 内测量的,使用我桌面上运行 20 次以上的最小挂钟时间。根据偶尔的一瞥,top它似乎总是在单核上运行。

对于好奇:分析

我不确定 Python 版本是否重要,大多数工作应该在 C 库中进行。为了验证这一点,我分析了pickle + lz4变体:

perf record ./test.py && perf report -s dso
Overhead  Shared Object
  60.16%  [kernel.kallsyms]  # mostly page_fault and alloc_pages_vma
  27.53%  libc-2.28.so       # mainly memmove
   9.75%  liblz4.so.1.8.3    # only LZ4_decompress_*
   2.33%  python3.7
   ...

大多数时间都花在 Linux 内核中,做page_fault与(重新)分配内存相关的事情,可能包括磁盘 I/O。大量的memmove看起来可疑。每次新的解压缩块到达时,Python 可能都会重新分配(调整大小)最终数组。如果有人想仔细看看:python 和 perf 配置文件

于 2019-06-22T06:22:53.017 回答
4

您可以继续使用现有的 PNG 并享受节省空间的乐趣,但使用libvips. 这是一个比较,但我并没有测试我的笔记本电脑和你的笔记本电脑的速度,而是展示了 3 种不同的方法,这样你就可以看到相对速度。我用了:

  • 太平船务
  • 开放式CV
  • pyvips

#!/usr/bin/env python3

import numpy as np
import pyvips
import cv2
from PIL import Image

def usingPIL(f):
    im = Image.open(f)
    return np.asarray(im)

def usingOpenCV(f):
    arr = cv2.imread(f,cv2.IMREAD_UNCHANGED)
    return arr

def usingVIPS(f):
    image = pyvips.Image.new_from_file(f)
    mem_img = image.write_to_memory()
    imgnp=np.frombuffer(mem_img, dtype=np.uint8).reshape(image.height, image.width, 3) 
    return imgnp

然后我检查了 IPython 的性能,因为它有很好的计时功能。如您所见,pyvips由于避免了数组复制,即使 PIL 比原始版本快 2 倍,它也比 PIL 快 13 倍:

In [49]: %timeit usingPIL('Alaska1.png')                                                            
3.66 s ± 31.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [50]: %timeit usingOpenCV('Alaska1.png')                                                         
6.82 s ± 23.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [51]: %timeit usingVIPS('Alaska1.png')                                                           
276 ms ± 4.24 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# Quick test results match
np.sum(usingVIPS('Alaska1.png') - usingPIL('Alaska1.png')) 
0
于 2019-06-22T20:11:53.743 回答
0

我认为应该很快的是

  1. 使用 gzip(或其他)进行压缩
  2. 直接将压缩数据作为文字字节存储在 python 模块中
  3. 将解压后的形式直接加载到 numpy 数组中

即编写一个生成源代码的程序,例如

import gzip, numpy
data = b'\x00\x01\x02\x03'
unpacked = numpy.frombuffer(gzip.uncompress(data), numpy.uint8)

打包的数据最终直接编码到 .pyc 文件中

对于低熵数据,gzip解压应该相当快(编辑:毫不奇怪lzma甚至更快,而且它仍然是一个预定义的 python 模块)

使用您的“阿拉斯加”数据,这种方法在我的机器上提供以下性能

compression   source module size   bytecode size   import time
-----------   ------------------   -------------   -----------
gzip -9               26,133,461       9,458,176          1.79
lzma                  11,534,009       2,883,695          1.08

你甚至可以只分发 .pyc,只要你可以控制所使用的 python 版本;在 Python 2 中加载 .pyc 的代码是单行代码,但现在更加复杂(显然,加载 .pyc 不应该很方便)。

请注意,该模块的编译速度相当快(例如,lzma 版本在我的机器上编译只需 0.1 秒),但很遗憾无缘无故地在磁盘上浪费了 11Mb。

于 2019-06-21T21:04:26.773 回答