我使用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}}被调用时,整个内存被释放。
我的错误在哪里导致图表被复制并在内存中存在两次?
答案 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)。