簡體   English   中英

用Python節省空間的Cython對象酸洗?

[英]Space-efficient pickling of Cython objects in Python?

我正在嘗試找到一種節省空間的方法,以便在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

但是令人驚訝的是,盡管進行了靜態鍵入和較輕的內存表示,但經過酸洗后,cythonized版本仍占用更多空間。

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

有沒有更有效的方式來序列化cython對象?


跟進

我想保留個人原因CPoint對象是因為我收到異構CPoint -樣流應用的對象,所以我需要緩沖它們在list異質類型。

如果我們可以保證列表元素的類型,那么使用numpy數組確實可以改善存儲空間。 使用同質容器,我們也有可能獲得更好的壓縮屬性,但是您將不得不放棄序列化非結構化數據的多功能性。

可以使用@ead和@DavidW提出的同質容器的空間優勢,同時容納非結構化數據的一種算法解決方案是在前面存儲對象位置的位圖(假設我們知道所有可能的傳入對象類型字節碼編譯時間(這是一個廣泛的假設),然后仍將對象分組在同質容器中。 也許有可能通過以面向列的方式對它們進行排序來進一步提高效率,從而使壓縮效果更好。 沒有基准測試很難說。

這不是專門針對Cython的解決方案,但:如果您擔心磁盤的大小,那么您可能會發現很多。 在這種情況下,一個不錯的選擇是將數據存儲在一個numpy結構化數組中,以避免創建大量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位int來存儲,而且我懷疑這是否容易手動打敗)

點5.9775

積分10.47975

nparray 8.0107

一方面,此答案應該是@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 ,這很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)]

並比較PointCPointint32結構的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的答案不同,我查看的是整個對象的大小,而不是每個整數值),這與它獲得的效果一樣好。 這是由於以下事實:我使用np.int32而不是int (通常為64位)作為坐標。

重要一點:numpy數組即使只有很小的坐標,也仍然比Point列表更好-在這種情況下,大小約為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 

Point還差2個字節,但比原始版本好7個字節。 還有一個好處是,我們可以從較小的整數中受益,而較小的整數將為您帶來好處-但仍然比Point -version少2個字節。

另一種方法是定義一個專用的CPoints類/包裝器列表:

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM