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

时间:2013-11-21 14:32:59

标签: c++ python memory-management cython

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

图表类的我的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内存。 <{1}}被调用时,整个内存被释放。

我的错误在哪里导致图表被复制并在内存中存在两次?

3 个答案:

答案 0 :(得分:3)

我没有给你一个确定的答案,但我有一个理论。

你编写的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个字节。

我希望这有帮助!

答案 1 :(得分:0)

您需要修改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;
}

答案 2 :(得分:-3)

如果您的约束/目标是“在合理的时间内,在单个PC上计算具有数十亿边缘的图表。”,请考虑重构以利用GraphChi

如果单机/内存不是约束,请考虑利用像Neo4j这样的图形数据库,而不是将所有数据都拉入内存。还有与Hadoop重叠的图形API(例如Apache Giraph)。