Относительная производительность декомпрессии Python? - PullRequest
4 голосов
/ 21 июня 2019

TLDR; Из различных алгоритмов сжатия, доступных в python gzip, bz2, lzma и т. Д., Которые имеют лучшую декомпрессионную производительность?

Полная дискуссия:

Python 3 имеет различных модулей для сжатия / распаковки данных в том числе gzip, bz2 и lzma. gzip и bz2 дополнительно имеют различные уровни сжатия, которые вы можете установить.

Если моя цель - сбалансировать размер файла (/ коэффициент сжатия) и скорость распаковки (скорость сжатия не имеет значения), какой будет лучшим выбором? Скорость распаковки важнее, чем файл размер, но поскольку размер несжатых файлов будет около 600-800 МБ каждый (32-битные файлы изображений RGB .png), и у меня их дюжина, я хочу некоторое сжатие.

  • Мой пример использования: я загружаю дюжину изображений с диска, выполняю некоторую обработку на них (в виде пустого массива) и затем использую данные обработанного массива в моей программе.

    • Изображения никогда не меняются, мне просто нужно загружать их каждый раз, когда я запускаю свою программу.
    • Обработка занимает примерно столько же времени, сколько загрузка (несколько секунд), поэтому я пытаюсь сэкономить некоторое время загрузки, сохраняя обработанные данные (используя pickle), а не загружая необработанные, необработанные изображения каждый раз. Начальные тесты были многообещающими - загрузка необработанных / несжатых маринованных данных заняла менее секунды, в отличие от 3 или 4 секунд, чтобы загрузить и обработать исходное изображение - но, как уже упоминалось, размер файла составлял около 600-800 МБ, тогда как исходные изображения png только около 5 МБ. Поэтому я надеюсь, что смогу найти баланс между временем загрузки и размером файла, сохранив выбранные данные в сжатом формате.
  • ОБНОВЛЕНИЕ: Ситуация на самом деле немного сложнее, чем я представлял выше. Мое приложение использует PySide2, поэтому у меня есть доступ к библиотекам Qt.

    • Если я читаю изображения и преобразовываю их в массив с использованием pillow (PIL.Image), мне фактически не нужно выполнять какую-либо обработку, но общее время считывания изображения в массив составляет около 4 секунд. .
    • Если вместо этого я использую QImage для чтения изображения, мне нужно будет выполнить некоторую обработку результата, чтобы сделать его пригодным для использования в остальной части моей программы из-за порядка, в котором QImage загружает данные - в основном я должен поменять порядок битов и затем повернуть каждый «пиксель» так, чтобы альфа-канал (который, очевидно, добавлен 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)

эта загрузка занимает около 4,2 с на моей машине с Python 3.7.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)

Загрузка из этого несжатого файла маринада занимает ~ 0,8 с на моей машине.

Ответы [ 4 ]

3 голосов
/ 22 июня 2019

низко висящий фрукт

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

Загрузка в 2,3 раза быстрее, чем ваш код на основе PIL.

Используется zipfile.ZIP_DEFLATED, см. savez_compressed document.

Ваш код PIL также содержит ненужную копию: array(img) должно быть asarray(img). Это стоит всего 5% времени медленной загрузки. Но после оптимизации это будет важно, и вы должны иметь в виду, какие операторы будут создавать копии.

Быстрая декомпрессия

Согласно тестам zstd , при оптимизации для декомпрессии 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, большая часть работы должна выполняться внутри библиотек Си. Чтобы проверить это, я профилировал 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 и другими делами, связанными с (пере) распределением памяти, возможно, включая дисковый ввод-вывод. Большое количество memmove выглядит подозрительно. Вероятно, Python перераспределяет (изменяет размер) окончательный массив каждый раз, когда приходит новый распакованный кусок. Если кто-то любит поближе взглянуть: Python и Perf профили .

2 голосов
/ 25 июня 2019

Вы можете использовать Python-blosc

Это очень быстро и для небольших массивов (<2 ГБ) также довольно прост в использовании. На легко сжимаемых данных, таких как ваш пример, часто быстрее сжимать данные для операций ввода-вывода. (SATA-SSD: около 500 МБ / с, PCIe-SSD: до 3500 МБ / с) На этапе декомпрессии распределение массива является наиболее затратной частью. Если ваши изображения имеют одинаковую форму, вы можете избежать повторного выделения памяти. </p>

Пример

Для следующего примера предполагается непрерывный массив.

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

Задержка

2 голосов
/ 22 июня 2019

Вы можете продолжать использовать существующие 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 в 13 раз быстрее, чем PIL, даже с 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
0 голосов
/ 22 июня 2019

Что-то, что я думаю, должно быть быстрым,

  1. используйте gzip (или другой) для сжатия
  2. напрямую сохраняет сжатые данные в модуле python в виде буквенных байтов
  3. напрямую загружать распакованную форму в массив numy

т.е.. написать программу, которая генерирует исходный код, такой как

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; код для загрузки .pyc в Python 2 был однострочным, но теперь он более запутанный (очевидно, было решено, что загрузка .pyc не должна быть удобной).

Обратите внимание, что компиляция модуля выполняется достаточно быстро (например, версия lzma компилируется на моей машине всего за 0,1 секунды), но жалко тратить на диск 11 Мб больше без какой-либо реальной причины.

...