Cython:如何移动大型对象而不复制它们

Cython: How to move large objects without copying them?

本文关键字:对象 复制 大型 何移动 移动 Cython      更新时间:2023-10-16

我使用Cython包装C++代码,并将其公开给Python以进行交互式工作。我的问题是,我需要从文件中读取大的图形(几GB),它们最终会在内存中出现两次。有人能帮我诊断和解决这个问题吗?

我的图类的Cython包装器如下所示:

cdef extern from "../src/graph/Graph.h":
    cdef cppclass _Graph "Graph":
        _Graph() except +
        _Graph(count) except +
        count numberOfNodes() except +
        count numberOfEdges() except +

cdef class Graph:
    """An undirected, optionally weighted graph"""
    cdef _Graph _this
    def __cinit__(self, n=None):
        if n is not None:
            self._this = _Graph(n)
    # any _thisect which appears as a return type needs to implement setThis
    cdef setThis(self, _Graph other):
        #del self._this
        self._this = other
        return self
    def numberOfNodes(self):
        return self._this.numberOfNodes()
    def numberOfEdges(self):
        return self._this.numberOfEdges()

如果需要返回Python Graph,则需要将其创建为空,然后使用setThis方法设置本机_Graph实例。例如,当从文件中读取Graph时,就会发生这种情况。这就是这个班的工作:

cdef extern from "../src/io/METISGraphReader.h":
    cdef cppclass _METISGraphReader "METISGraphReader":
        _METISGraphReader() except +
        _Graph read(string path) except +
cdef class METISGraphReader:
    """ Reads the METIS adjacency file format [1]
        [1]: http://people.sc.fsu.edu/~jburkardt/data/metis_graph/metis_graph.html
    """
    cdef _METISGraphReader _this
    def read(self, path):
        pathbytes = path.encode("utf-8") # string needs to be converted to bytes, which are coerced to std::string
        return Graph(0).setThis(self._this.read(pathbytes))

交互式用法如下:

 >>> G = graphio.METISGraphReader().read("giant.metis.graph")

从文件读取完成并使用X GB内存后,会出现明显的复制阶段,然后使用2X GB内存。调用del G时,将释放整个内存。

导致图形在内存中被复制和存在两次的错误在哪里?

我没有明确的答案,但我有一个理论。

您编写的Cython包装器是不寻常的,因为它们直接包装C++对象,而不是指向它的指针

以下代码效率特别低:

cdef setThis(self, _Graph other):
    self._this = other
    return self 

原因是_Graph类包含多个STL向量,这些向量必须被复制。因此,当您的other对象被分配给self._this时,内存使用量实际上会加倍(或者更糟,因为STL分配器可能会由于性能原因而过度分配)。

我编写了一个简单的测试,与您的测试相匹配,并在各处添加了日志记录,以查看对象是如何创建、复制或销毁的。我在那里找不到任何问题。复制确实发生了,但在任务完成后,我看到只剩下一个对象。

所以我的理论是,你看到的额外内存与向量中的STL分配器逻辑有关。所有额外的内存都必须在复制后附加到最终对象上。

我的建议是切换到更标准的基于指针的包装。您的_Graph包装器应该或多或少地定义如下:

cdef class Graph:
    """An undirected, optionally weighted graph"""
    cdef _Graph* _this
    def __cinit__(self, n=None):
        if n is not None:
            self._this = new _Graph(n)
        else:
            self._this = 0
    cdef setThis(self, _Graph* other):
        del self._this
        self._this = other
        return self
    def __dealloc__(self):
        del self._this

注意,我需要删除_this,因为它是一个指针。

然后,您需要修改METISGraphReader::read()方法,以返回堆分配的Graph。这种方法的原型应该改为:

Graph* METISGraphReader::read(std::string path);

然后它的Cython包装可以写成:

    def read(self, path):
        pathbytes = path.encode("utf-8") # string needs to be converted to bytes, which are coerced to std::string
        return Graph().setThis(self._this.read(pathbytes))

如果这样做,则只有一个对象,即read()在堆上创建的对象。指向该对象的指针返回到read()Cython包装器,然后该包装器将其安装在一个全新的Graph()实例中。唯一被复制的是指针的4或8个字节。

我希望这能有所帮助!

您需要修改C++类以通过shared_ptr存储其数据。确保你有一个合适的复制构造函数和赋值运算符:

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <memory>
struct Data { // your graph data
    Data(const char* _d = NULL) {
        if (_d)
            strncpy(d, _d, sizeof(d)-1);
        else
            memset(d, 0, sizeof(d));
    }
    Data(const Data& rhs) {
        memcpy(d, rhs.d, sizeof(d));
    }
    ~Data() {
        memset(d, 0, sizeof(d));
    }
    void DoSomething() { /* do something */ } // a public method that was used in Python
    char d[1024];
};
class A { // the wrapper class
public:
    A() {}
    A(const char* name)  : pData(new Data(name)) {}
    A(const A& rhs) : pData(rhs.pData) {}
    A& operator=(const A& rhs) {
        pData = rhs.pData;
        return *this;
    }
    ~A() {}
    // interface with Data
    void DoSomething() {
        if (pData.get() != NULL)
            pData->DoSomething();
    }
private:
    std::shared_ptr<Data> pData;
};
int main(int argc, char** argv)
{
    A o1("Hello!");
    A o2(o1);
    A o3;
    o3 = o2;
    return 0;
}

如果您的约束/目标是"在一台电脑上,在合理的时间内,在具有数十亿条边的图上进行计算",请考虑重构以利用GraphChi。

如果单机/内存不是一个限制,可以考虑利用像Neo4j这样的图形数据库,而不是将所有数据拉入内存。还有一些图形API覆盖Hadoop(例如ApacheGiraph)。