简体   繁体   English

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

[英]Cython: How to move large objects without copying them?

I use Cython to wrap C++ code and expose it to Python for interactive work. 我使用Cython来包装C ++代码并将其公开给Python以进行交互式工作。 My problem is that I need to read large graphs (several gigabytes) from file and they end up twice in the memory. 我的问题是我需要从文件中读取大图(几千兆字节),它们最终会在内存中两次。 Can anyone help me diagnose and solve this problem? 任何人都可以帮我诊断并解决这个问题吗?

My Cython wrapper for the graph class looks like this: 我的图形类的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()

If a Python Graph needs to be returned, it needs to be created empty and then the setThis method is used to set the native _Graph instance. 如果需要返回Python Graph,则需要将其创建为空,然后使用setThis方法设置本机_Graph实例。 This happens, for example, when a Graph is read from file. 例如,当从文件中读取Graph时会发生这种情况。 This is the job of this class: 这是这堂课的工作:

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

Interactive usage looks like this: 交互式用法如下所示:

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

After the reading from file is done and X GB memory are used, there is a phase where obviously copying happens, and after that 2X GB memory are used. 在完成从文件读取并使用X GB内存之后,存在明显复制发生的阶段,之后使用2X GB内存。 The entire memory is freed when del G is called. 调用del G时释放整个内存。

Where is my error which leads to the graph being copied and existing twice in memory? 我的错误导致图表被复制并在内存中存在两次?

I don't have a definitive answer for you, but I have a theory. 我没有给你一个确定的答案,但我有一个理论。

The Cython wrappers that you wrote are unusual, in that they wrap the C++ object directly instead of a pointer to it. 你编写的Cython包装器是不寻常的,因为它们直接包装C ++对象而不是指向它的指针。

The following code is particularly inefficient: 以下代码效率特别低:

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

The reason is that your _Graph class contains several STL vectors, and those will have to be copied over. 原因是你的_Graph类包含几个STL向量,并且必须复制这些向量。 So, when your other object is assigned to self._this the memory usage is effectively doubled (or worse, since the STL allocators can overallocate for performance reasons). 因此,当您的other对象被分配给self._this ,内存使用量实际上会翻倍(或者更糟,因为STL分配器可能因性能原因而过度分配)。

I wrote a simple test that matches yours and added logging everywhere to see how objects are created, copied or destroyed. 我编写了一个与您匹配的简单测试,并在各处添加了日志记录,以查看对象的创建,复制或销毁方式。 I can't find any issues there. 我在那里找不到任何问题。 The copies do happen, but after the assignment is complete I see that only one object remains. 副本确实发生了,但在完成任务后,我发现只剩下一个对象。

So my theory is that the extra memory that you see is related to STL allocator logic in the vectors. 所以我的理论是你看到的额外内存与向量中的STL分配器逻辑有关。 All that extra memory must be attached to the final object after the copies. 所有额外的内存必须在副本之后附加到最终对象。

My recommendation is that you switch to the more standard pointer based wrapping. 我的建议是你切换到更标准的基于指针的包装。 Your _Graph wrapper then should be defined more or less as follows: 那么你的_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

Note that I need to delete _this because it is a pointer. 请注意,我需要删除_this因为它是一个指针。

You will then need to modify your METISGraphReader::read() method to return a heap allocated Graph . 然后,您需要修改METISGraphReader::read()方法以返回堆分配的Graph The prototype of this method should be changed to: 此方法的原型应更改为:

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

Then the Cython wrapper for it can be written as: 然后它的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))

If you do it this way there is only one object, the one that is created on the heap by read() . 如果你这样做,那么只有一个对象,即read()在堆上创建的对象。 A pointer to that object is returned to the read() Cython wrapper, which then installs it in a brand new Graph() instance. 指向该对象的指针返回给read() Cython包装器,然后将其安装在一个全新的Graph()实例中。 The only thing that gets copied is the 4 or 8 bytes of the pointer. 唯一被复制的是指针的4或8个字节。

I hope this helps! 我希望这有帮助!

You'll need to modify the C++ class to store it's data via a shared_ptr. 您需要修改C ++类以通过shared_ptr存储它的数据。 Make sure you have a proper copy constructor and assignment operator: 确保你有一个合适的拷贝构造函数和赋值运算符:

#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;
}

If your constraint/goal is “Compute on graphs with billions of edges, in a reasonable time, on a single PC.” , consider refactoring to leverage GraphChi . 如果您的约束/目标是“在合理的时间内,在单个PC上计算具有数十亿边缘的图形。” ,请考虑重构以利用GraphChi

If single-machine/in-memory is not a constraint, consider leveraging a graph database like Neo4j instead of pulling all data into memory. 如果单机/内存不是约束,请考虑利用像Neo4j这样的图形数据库,而不是将所有数据都拉入内存。 There are also graph APIs that overlay with Hadoop (eg Apache Giraph ). 还有与Hadoop重叠的图形API(例如Apache Giraph )。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM