Экономичное пространство для травления объектов Cython в Python? - PullRequest
0 голосов
/ 02 июня 2018

Я пытаюсь найти эффективный для хранения способ хранения объекта, похожего на структуру, в Python.

# file point.py

import collections
Point = collections.namedtuple('Point', ['x', 'y'])

Вот цитатная версия:

# file cpoint.pyx

cdef class CPoint:

    cdef readonly int x
    cdef readonly int y

    def __init__(self, int x, int y):
        self.x = x
        self.y = y

    def __repr__(self):
        return 'Point(x={}, y={})'.format(self.x, self.y)

Я ожидаюЦитонизированная версия для более эффективного использования памяти:

from pympler.asizeof import asizeof
from point import Point
from cpoint import CPoint

asizeof(Point(1,2))     # returns 184
asizeof(CPoint(1,2))    # returns 24

Но удивительно, что цитонизированная версия занимает больше места при протравливании, несмотря на статическую типизацию и более легкое представление в памяти.

import pickle
len(pickle.dumps(Point(1,2)))     # returns 28
len(pickle.dumps(CPoint(1,2)))    # returns 70

IsЕсть ли более эффективный способ сериализации объектов Cython, как этот?


Последующие действия

Причина, по которой я хотел бы сохранить отдельные CPoint объекты, заключается в том, что я получаюгетерогенные CPoint -подобные объекты в потоковом приложении, поэтому мне нужно буферизовать их в list гетерогенных типов.

Действительно, можно увеличить объем хранилища, используя массивы numpy, если бы у нас были гарантии натипы элементов списка.Также вероятно, что мы получим лучшие свойства сжатия с однородным контейнером, но вам придется отказаться от универсальности сериализации неструктурированных данных.

Одно алгоритмическое решение, которое использует пространственное преимущество однородных контейнеров.предложенные @ead и @DavidW при размещении неструктурированных данных должны были бы хранить растровое изображение положений объектов впереди (предполагая, что мы знаем все возможные типы входящих объектов во время компиляции байт-кода, что является широким допущением), тогда ещесгруппировать объекты в однородные контейнеры.И, возможно, можно извлечь дополнительную эффективность, упорядочив их по столбцам, чтобы сжатие лучше их подобрало.Трудно сказать без бенчмаркинга.

Ответы [ 2 ]

0 голосов
/ 05 июня 2018

С одной стороны, этот ответ должен быть дополнением к ответу @DavidW, но с другой стороны, он также исследует возможные улучшения.В нем также предлагается использовать оболочку для сериализации, которая сохранит любимые объекты CPoint, но обеспечивает ту же плотную сериализацию, что и структурированные numpy-массивы.

Как уже указывалось, сравнивать не имеет особого смысла.размеры одного сериализованного объекта - это слишком много накладных расходов.Помимо прочего, Python должен сохранить идентификатор класса, который представляет собой полное имя модуля + имя класса.В моем случае я использую ipython с %% cython-magic, он довольно длинный:

>>> print(pickle.dumps(CPoint(1,2)))
b'\x80\x03c_cython_magic_46e1a18d1df9b5ea5ee974991f9aba67\n__pyx_unpickle_CPoint\nq\x00c_cython_magic_46e1a18d1df9b5ea5ee974991f9aba67\nCPoint\nq\x01J\xe9\x1a\x8d\x0cK\x01K\x02\x86q\x02\x87q\x03Rq\x04.'

Длина автоматически создаваемого имени модуля равна c_cython_magic_46e1a18d1df9b5ea5ee974991f9aba67, и это больно!

Таким образом, в принципе, не зная, как хранятся ваши объекты (список, карта, набор или что-то еще), невозможно дать правильный ответ.

Однако, подобно @DavidW, завещаниеПредположим, что точки хранятся в списке.Когда в списке несколько CPoint объектов, pickle достаточно умен, чтобы сохранить заголовок класса только один раз.

Я выбираю немного другую тестовую установку - координаты выбираются случайным образом из диапазона[-2e9,2e9], который в основном охватывает весь диапазон int32 (приятно знать, что pickle достаточно умен, чтобы уменьшить количество необходимых байтов для малых значений, но насколько велико усиление, зависит от распределения точек):

N=10000
x_lst=[random.randint(-2*10**9, 2*10**9) for _ in range(N)]
y_lst=[random.randint(-2*10**9, 2*10**9) for _ in range(N)]

и сравнение списков массивов со структурой Point s, CPoint s и int32:

lst_p  = [ Point(x,y)  for x,y in zip(x_lst, y_lst)]
lst_cp = [ CPoint(x,y) for x,y in zip(x_lst, y_lst)]
lst_np = np.array(list(zip(x_lst, y_lst)), dtype=[('x',np.int32),('y',np.int32)])

, которые дают следующие результаты:

 print("Point", len(pickle.dumps(lst_p,protocol=pickle.HIGHEST_PROTOCOL))/N)   
 print("CPoint", len(pickle.dumps(lst_cp,protocol=pickle.HIGHEST_PROTOCOL))/N)    
 print("nparray", len(pickle.dumps(lst_np,protocol=pickle.HIGHEST_PROTOCOL))/N)

 Point 16.0071
 CPoint 25.0145 
 nparray 8.0213

Это означает, что nparray требуется только 8 байтов на запись (в отличие от ответов @ DavidW, я смотрю на размер всего объекта, а не на целочисленное значение), что так же хорошо, как и получается.Это связано с тем, что я использую np.int32, а не int (обычно это 64 бита) для координат.

Один важный момент: numpy-массивы все же лучше, чем список Point s, даже если бы они имели только маленькие координаты - в этом случае размер был бы около 12 байтов, как показали эксперименты @ DavidW.

Но можно было бы любить объекты CPoint больше, чем структуры numpy.Итак, какие еще варианты у нас есть?

Легкая возможность - не использовать автоматически созданную функцию травления, а сделать это вручную:

%%cython
cdef class CPoint:
    ...

    def __getstate__(self):
        return (self.x, self.y)

    def __setstate__(self, state):
        self.x, self.y=state

А теперь:

 >>> pickle.loads(pickle.dumps(CPoint(1,3)))
 Point(x=1, y=3)
 >>> print("CPoint", len(pickle.dumps(lst_cp,protocol=pickle.HIGHEST_PROTOCOL))/N)  
 CPoint 18.011 

Все еще на 2 байта хуже, чем Point, но также на 7 байтов лучше, чем оригинальная версия.Плюсом также является то, что мы получили бы выгоду от меньшего размера для меньших целых чисел - но все равно оставались бы на 2 байта в стороне от Point -версии.

Другой подход заключается в определении выделенного списка CPoints.-class / wraper:

%% массива импорта cython
класс cdef CPointListWrapper: список cdef lst def init (self, lst): self.lst = lst

def release_list(self):
    result=self.lst
    self.lst=[]
    return result

def __getstate__(self):    
    output=array.array('i',[0]*(2*len(self.lst)))
    for index,obj in enumerate(self.lst):
        output[index*2]  =obj.x
        output[index*2+1]=obj.y
    return output

def __setstate__(self, in_array):
    self.lst=[]
    n=len(in_array)//2
    for i in range(n):
        self.lst.append(CPoint(in_array[2*i], in_array[2*i+1]))    

Это очевидно быстро и грязно, и многое можно улучшить с точки зрения производительности, но я надеюсь, что вы поняли суть!А теперь:

 >>> print("CPointListWrapper", len(pickle.dumps(CPointListWrapper(lst_cp),protocol=pickle.HIGHEST_PROTOCOL))/N)
 CPoint 8.0149

так же хорошо, как numpy, но прилипает к CPoint-объектам!Он также работает правильно:

>>> pickle.loads(pickle.dumps(CPointListWrapper([CPoint(1,2), CPoint(3,4)]))).release_list()
[Point(x=1, y=2), Point(x=3, y=4)]
0 голосов
/ 03 июня 2018

Это не совсем решение Cython, но: возможно, если вас беспокоит размер диска, у вас их много.В этом случае хорошим вариантом является сохранение данных в numpy структурированном массиве , чтобы избежать создания большого количества объектов Python (или, возможно, чего-то вроде Pandas).

Я бы также ожидал засолкумассив / numy список объектов должен быть более полезным представлением размера, чем выбор отдельного объекта (я считаю, pickle делает некоторые оптимизации, когда у вас много одного и того же)

import collections
from cpoint import CPoint

Point = collections.namedtuple('Point', ['x', 'y'])

l = [ Point(n,n) for n in range(10000) ]
l2 = [ CPoint(n,n) for n in range(10000) ]

import numpy as np
l3 = np.array(list(zip(list(range(10000)), list(range(10000)))),
              dtype=[('x',int),('y',int)])

import pickle
print("Point",len(pickle.dumps(l))/20000)
print("CPoint",len(pickle.dumps(l2))/20000)
print("nparray",len(pickle.dumps(l3))/20000)

Этоотпечатки:

Точка 9.9384

CPoint 16.4402

nparray 8.01215

Версии namedtuple и numpy.array довольно симпатичныблизко к ожидаемому предельному пределу в 8 байт на int, но версия с массивным массивом лучше.


Интересно, что если мы добавим protocol=pickle.HIGHEST_PROTOCOL к вызову, все улучшится и namedtupleверсия снова побеждает убедительно.(Я подозреваю, что он заметил, что для хранения не нужны полные 64-битные целые, и я сомневаюсь, что это будет легко сделать вручную)

Точка 5.9775

CPoint 10.47975

nparray 8.0107

...