如何用相同的 cython memoryview 的许多视图来腌制对象

How to pickle objects with many views of same cython memoryview

我有许多我试图腌制的对象,它们都共享相同的(大)cython memoryview 作为属性。由于内存视图是通过引用传递的,因此它们都共享相同的内存并且实现是内存高效的。

现在我需要 pickle 这些对象并重新加载它们,同时保持共享数据共享(如果共享数据变得不共享,那么文件大小会爆炸并且无法读入内存)。通常我认为 pickle 识别共享数据并且只 pickles/unpickles 它一次,但是因为不能直接 pickle 内存视图,所以需要在 reduce 中将它们转换为 numpy 数组每个对象和 pickle 的方法不再识别数据是共享的。

有什么方法可以通过 pickle/unpickle 进程维护共享数据?

A MWE 如下:

import numpy as np    
import pickle

cdef class SharedMemory:  
    cdef public double[:, :] data

    def __init__(self, data):   
        self.data = data

    def duplicate(self):
        return SharedMemory(self.data)

    def __reduce__(self):   
        return self.__class__, (np.asarray(self.data),)


def main():   
    x = SharedMemory(np.random.randn(100, 100))

    duplicates = [x.duplicate() for _ in range(5)]

    cdef double* pointerx = &x.data[0, 0]
    cdef double* pointerd
    cdef double[:, :] ddata

    for d in duplicates:
        ddata = d.data 
        pointerd = &ddata[0, 0]
        if pointerd != pointerx:
            print('Memory is not shared')
        else:
            print('Memory is shared')

    print('pickling')
    with open('./temp.pickle', 'wb') as pfile:
        pickle.dump(x, pfile, protocol=pickle.HIGHEST_PROTOCOL)
        for d in duplicates:
            pickle.dump(d, pfile, protocol=pickle.HIGHEST_PROTOCOL)

    with open('./temp.pickle', 'rb') as pfile:
        nx = pickle.load(pfile)
        nd = []
        for d in duplicates:
            nd.append(pickle.load(pfile))

    ddata = nx.data
    cdef double* pointernx = &ddata[0, 0]

    for d in nd:
        ddata = d.data
        pointerd = &ddata[0, 0]
        if pointerd != pointernx:
            print('Memory is not shared')
        else:
            print('Memory is shared')

将以上内容放入文件 test.pyx 中,使用 "cythonize -a -i test.pyx" 进行 cythonize。然后 "export PYTHONPATH="$PYTHONPATH":"和 运行

from test import main
main()

来自 python。

其实有两个问题:

首先: 共享对象在 dump/load 之后也被共享,只有当它们被一次腌制时(另见 this answer)。

这意味着您需要执行以下操作(或类似操作):

...
with open('./temp.pickle', 'wb') as pfile:
     pickle.dump((x,duplicates), pfile, protocol=pickle.HIGHEST_PROTOCOL)
...
with open('./temp.pickle', 'rb') as pfile:
     nx, nd = pickle.load(pfile)
...

当您转储单个对象时,pickle 无法跟踪相同的对象 - 这样做会成为一个问题:两个 dump 调用之间具有相同 ID 的对象可能是完全不同的对象,也可能是相同的对象不同内容的对象!

其次: 你不应该创建新对象,而是在 __reduce__ 中传递共享的 numpy 对象(pickle 不会查看 numpy 数组内部以看,缓冲区是否共享,但仅在数组的 id 处):

def __reduce__(self):
    return self.__class__, (self.data.base,)

这会给你想要的结果。 data.base 是对底层原始 numpy 数组(或任何类型,显然必须支持 pickling/unpickling)的引用。


警告: 正如@DavidW 正确指出的那样,在使用切片内存视图时必须考虑其他注意事项 - 因为在这种情况下 base可能不是 "the same" 作为实际内存视图。