在Python中节省空间的Cython对象腌制?

时间:2018-06-02 18:49:31

标签: python struct cython pickle namedtuple

我正在尝试找到一种节省空间的方法来在Python中存储类似结构的对象。

# file point.py

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

这是cythonized版本:

# 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)

我希望cythonized版本的内存效率更高:

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

但令人惊讶的是,尽管静态打字和内存表现较轻,但cython化版本在腌制时会占用更多空间。

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

有没有更有效的方法来序列化像这样的cython对象?

后续

我想保留单个CPoint对象的原因是因为我在流式应用程序中收到类似异性CPoint的对象,所以我需要在{{1}中缓冲它们异类型。

如果我们对列表元素的类型有保证,确实可以使用numpy数组来改善存储空间。我们也可能会使用同质容器获得更好的压缩属性,但您必须放弃序列化非结构化数据的多功能性。

一种算法解决方案可以回归@ead和@DavidW提出的同质容器的空间优势,同时适应非结构​​化数据将存储前面对象位置的位图(假设我们知道所有可能的类型在字节码编译时传入的对象,这是一个广泛的假设),然后仍然将对象分组在同质容器中。也许通过以列为导向的方式对它们进行排序可以提高效率,从而使压缩效果更好。没有基准测试就很难说。

2 个答案:

答案 0 :(得分:2)

这不是一个特别的Cython解决方案,但是:大概如果你担心磁盘上的大小,那么你有很多这些。在这种情况下,一个好的选择是将数据存储在numpy structured array中,以避免创建大量的Python对象(或者可能是像Pandas那样)。

我还希望腌制一个数组/ numpy对象列表是一个更有用的大小表示而不是腌制一个对象(我相信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

namedtuplenumpy.array版本都非常接近我们期望的每个int限制8个字节,但numpy数组版本更好。

有趣的是,如果我们在调用中添加protocol=pickle.HIGHEST_PROTOCOL,那么一切都会进一步改善,namedtuple版本会再次令人信服地获胜。 (我怀疑它注意到它不需要完整的64位整数存储,我怀疑这是否容易被手动击败)

  

点5.9775

     

CPoint 10.47975

     

nparray 8.0107

答案 1 :(得分:1)

一方面,这个答案应该是对@DavidW答案的补充,但另一方面它也会调查可能的改进。它还建议使用包装器进行序列化,这将保留心爱的CPoint对象,但实现与结构化numpy-arrays相同的密集序列化。

正如已经指出的那样,比较单个序列化对象的大小并没有多大意义 - 这只是过多的开销。除此之外,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,a将假设这些点存储在列表中。当列表中有多个CPoint个对象时,pickle足够聪明,只能保存一次Class-header。

我选择了一个稍微不同的测试设置 - 坐标是从[-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,CPointint32 - 结构化numpy数组的列表:

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' s答案不同,我看的是整个对象的大小,而不是每个整数值),这与它得到的一样好。这是因为我使用np.int32而不是int(通常是64个咬)作为坐标。

重要的一点是:numpy-arrays仍然比Point的列表更好,即使它们只有很小的坐标 - 在这种情况下,大小约为12个字节,就像@DavidW的实验一样已经表明了。

但是人们可能比CPoint-objects更喜欢CPoint-objects。那么我们还有哪些其他选择?

一个简单的可能性就是不使用自动创建的酸洗功能,而是手工完成:

%%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 

Point还差2个字节,但比原始版本好7个字节。一个加号也是我们可以从较小的整数中获得较小的大小 - 但仍然会比Point版本低2个字节。

另一种方法是定义一个专用的CPoints-class / wraper列表:

%%用Cython 导入阵列
cdef类CPointListWrapper:     cdef list 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-objects!它也正常工作:

>>> pickle.loads(pickle.dumps(CPointListWrapper([CPoint(1,2), CPoint(3,4)]))).release_list()
[Point(x=1, y=2), Point(x=3, y=4)]