Python解压缩的相对性能?

时间:2019-06-21 18:24:45

标签: python performance gzip lzma bz2

TLDR;在the various compression algorithms available in python gzipbz2lzma等中,减压的效果最佳?

完整讨论:

Python 3具有various modules for compressing/decompressing data 包括gzipbz2lzmagzipbz2还可设置不同的压缩级别。

如果我的目标是平衡文件大小(/压缩率)和解压缩速度(不考虑压缩速度),那将是最佳选择?解压缩速度比文件大小,但是由于每个未压缩文件的大小约为600-800MB(32位RGB .png图像文件),而且我有十几个文件,因此我确实希望 some 压缩。 / p>

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

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

    • 如果我读取图像并使用pillowPIL.Image)转换为numpy数组,则实际上我不需要进行任何处理,而是将图像读取到数组中的总时间大约4秒。
    • 如果我改为使用QImage来读取图像,则由于QImage的字节顺序,我必须对结果进行一些处理以使其可用于其余程序。加载数据-基本上,我必须交换位顺序,然后旋转每个“像素”,以使alpha通道(显然由QImage添加)位于最后而不是首先。整个过程大约需要3.8秒,因此,边际比仅使用PIL快。
    • 如果我将numpy数组保存为未压缩状态,那么我可以在.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图片样本:例如,以this 5.0Mb png image, a fairly high resolution image of the coastline of Alaska

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.2s。

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

从此未压缩的泡菜文件中加载文件大约需要0.8s。

4 个答案:

答案 0 :(得分:3)

低垂的果实

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 and perf profiles

答案 1 :(得分:2)

您可以继续使用现有的PNG并节省空间,但是使用libvips可以提高速度。这是一个比较,但我没有显示笔记本电脑与您的笔记本电脑的速度,而是显示了3种不同的方法,以便您可以查看相对速度。我用过:

  • PIL
  • OpenCV
  • 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快13倍,即使PIL比原始版本快2倍:

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

答案 2 :(得分:2)

您可以使用Python-blosc

它是very fast,对于小型阵列(<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

时间

答案 3 :(得分: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上浪费了资源。