Python解压相对性能?

ibr*_*ter 7 python performance gzip lzma bz2

TLDR;的python中提供的各种压缩算法 gzipbz2lzma,等,具有最佳的减压性能?

完整讨论:

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

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

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

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

    • 如果我读取图像并使用pillow( PIL.Image)转换为 numpy 数组,实际上我不需要做任何处理,但将图像读入数组的总时间约为 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     ? ?                      ? ?             ? ?           ?
?????????????????????????????????????????????????????????????????????
Run Code Online (Sandbox Code Playgroud)

.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)
Run Code Online (Sandbox Code Playgroud)

在我使用 Python 3.7.2 的机器上,此加载大约需要 4.2 秒。

或者,我可以加载通过选择上面创建的数组生成的未压缩的 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)
Run Code Online (Sandbox Code Playgroud)

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

max*_*111 6

你可以使用 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
Run Code Online (Sandbox Code Playgroud)

基准

#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
Run Code Online (Sandbox Code Playgroud)

编辑

这个版本更适合一般用途。它确实处理 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
Run Code Online (Sandbox Code Playgroud)

  • 为了在野外用作函数,压缩应该从“arr = np.ascontigouslyarray(arr)”开始,以防 arr 是数组的视图,并且在内存中不连续。我尝试编辑答案,但编辑队列已满。 (2认同)
  • @Atnas我添加了一些更高级的版本(数组&gt; 2GB,fortran-c连续数组和非连续数组) (2认同)

max*_*axy 5

低垂的果实

numpy.savez_compressed('AlaskaCoast.npz', arr)
arr = numpy.load('AlaskaCoast.npz')['arr_0']
Run Code Online (Sandbox Code Playgroud)

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

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

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

快速减压

根据zstd benchmarks,在优化解压时lz4是一个不错的选择。只需将其插入pickle 即可获得2.4 倍的增益,并且仅比未压缩的pickle 慢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)
Run Code Online (Sandbox Code Playgroud)

基准

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)
Run Code Online (Sandbox Code Playgroud)

加载时间是在 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
   ...
Run Code Online (Sandbox Code Playgroud)

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