用C++运行Python脚本时出现内存泄漏

8
以下是在C++中调用Python函数的最简示例,我的系统上存在内存泄漏问题: :
import tensorflow
def foo(param):
    return "something"

main.cpp:

#include "python3.5/Python.h"

#include <iostream>
#include <string>

int main()
{
    Py_Initialize();

    PyRun_SimpleString("import sys");
    PyRun_SimpleString("if not hasattr(sys,'argv'): sys.argv = ['']");
    PyRun_SimpleString("sys.path.append('./')");

    PyObject* moduleName = PyUnicode_FromString("script");
    PyObject* pModule = PyImport_Import(moduleName);
    PyObject* fooFunc = PyObject_GetAttrString(pModule, "foo");
    PyObject* param = PyUnicode_FromString("dummy");
    PyObject* args = PyTuple_Pack(1, param);
    PyObject* result = PyObject_CallObject(fooFunc, args);

    Py_CLEAR(result);
    Py_CLEAR(args);
    Py_CLEAR(param);
    Py_CLEAR(fooFunc);
    Py_CLEAR(pModule);
    Py_CLEAR(moduleName);

    Py_Finalize();
}

编译使用

g++ -std=c++11 main.cpp $(python3-config --cflags) $(python3-config --ldflags) -o main

并使用valgrind运行

valgrind --leak-check=yes ./main

产生以下摘要。
LEAK SUMMARY:
==24155==    definitely lost: 161,840 bytes in 103 blocks
==24155==    indirectly lost: 33 bytes in 2 blocks
==24155==      possibly lost: 184,791 bytes in 132 blocks
==24155==    still reachable: 14,067,324 bytes in 130,118 blocks
==24155==                       of which reachable via heuristic:
==24155==                         stdstring          : 2,273,096 bytes in 43,865 blocks
==24155==         suppressed: 0 bytes in 0 blocks

我正在使用 Linux Mint 18.2 Sonyag++ 5.4.0Python 3.5.2TensorFlow 1.4.1
移除 import tensorflow 后内存泄露问题得到了解决。是 TensorFlow 的一个 bug 还是我的操作有误?(我觉得后者更有可能。)
此外,当我在 Python 中创建一个 Keras 层时,...
#script.py
from keras.layers import Input
def foo(param):
    a = Input(shape=(32,))
    return "str"

并且重复地从C++中调用Python

//main.cpp

#include "python3.5/Python.h"

#include <iostream>
#include <string>

int main()
{
    Py_Initialize();

    PyRun_SimpleString("import sys");
    PyRun_SimpleString("if not hasattr(sys,'argv'): sys.argv = ['']");
    PyRun_SimpleString("sys.path.append('./')");

    PyObject* moduleName = PyUnicode_FromString("script");
    PyObject* pModule = PyImport_Import(moduleName);

    for (int i = 0; i < 10000000; ++i)
    {
        std::cout << i << std::endl;
        PyObject* fooFunc = PyObject_GetAttrString(pModule, "foo");
        PyObject* param = PyUnicode_FromString("dummy");
        PyObject* args = PyTuple_Pack(1, param);
        PyObject* result = PyObject_CallObject(fooFunc, args);

        Py_CLEAR(result);
        Py_CLEAR(args);
        Py_CLEAR(param);
        Py_CLEAR(fooFunc);
    }

    Py_CLEAR(pModule);
    Py_CLEAR(moduleName);

    Py_Finalize();
}

应用程序在运行时内存消耗不断增加,无限增长。

因此我猜测从C++调用Python函数的方式存在根本性错误,但是是什么呢?


你可以使用--trace-origin=yes来查看内存泄漏的分配位置。Python模块中经常有全局变量,在加载时初始化并永久存在,但这不是一个大问题,因为每个模块只会被加载一次。 - ead
你的第二个内存泄漏很奇怪,也许是Keras中的泄漏? 如果没有导入Keras或未创建Input,您是否看到内存泄漏? - ead
@ead 循环中不断增长的内存泄漏只会在我创建“Input”时发生。仅导入Keras就会导致第一个示例的内存泄漏。 - Tobias Hermann
@ead 内存泄漏跟踪总是显示原点。--trace-origin 用于未初始化的内存。引用手册中的一句话:“要查看程序中未初始化数据的来源信息,请使用 --track-origins=yes 选项。这会使 Memcheck 运行得更慢,但可以更轻松地跟踪未初始化值错误的根本原因。”来自 http://valgrind.org/docs/manual/mc-manual.html - Paul Floyd
1个回答

6

你的问题涉及到两种不同类型的"内存泄漏"。

Valgrind 正在告诉你有关第一种内存泄漏的情况。然而,Python 模块“泄漏”内存是非常普遍的,这主要是一些全局变量在模块加载时被分配/初始化。由于 Python 只会加载一次该模块,所以这并不是一个大问题。

一个众所周知的例子是 numpy 的PyArray_API:它必须通过 _import_array 进行初始化,然后永远不会被删除,直到 Python 解释器关闭。

因此,这是一种设计上的"内存泄漏",你可以争论它是否是一个好的设计,但最终你对此无能为力。

我对 tensorflow 模块没有足够的了解,无法确定这样的内存泄漏发生在哪些地方,但我很确定这不是你需要担心的事情。


第二个“内存泄漏”比较微妙。
当您比较循环的10^410^5次迭代的valgrind输出时,您可以获得引导-几乎没有区别!但是,最高内存消耗存在差异。
与C ++不同,Python具有垃圾收集器-因此您无法确定对象何时被销毁。 CPython使用引用计数,因此当引用计数为0时,对象将被销毁。但是,当存在引用循环(例如,对象A持有对象B的引用,并且对象B持有对象B的引用)时,情况并非如此简单:垃圾收集器需要遍历所有对象以查找不再使用的循环。
可以认为keras.layers.Input具有某个循环(这是真的),但这不是此“内存泄漏”的原因,它也适用于纯python。
我们使用objgraph包来检查引用,请运行以下python脚本:
#pure.py
from keras.layers import Input
import gc
import sys
import objgraph


def foo(param):
    a = Input(shape=(1280,))
    return "str"

###  MAIN :

print("Counts at the beginning:")
objgraph.show_most_common_types()
objgraph.show_growth(limit=7) 

for i in range(int(sys.argv[1])):
   foo(" ")

gc.collect()# just to be sure

print("\n\n\n Counts at the end")
objgraph.show_most_common_types()
objgraph.show_growth(limit=7)

import random
objgraph.show_chain(
   objgraph.find_backref_chain(
        random.choice(objgraph.by_type('Tensor')), #take some random tensor
         objgraph.is_proper_module),
    filename='chain.png') 

并运行它:

>>> python pure.py 1000

我们可以看到以下内容:最后有正好 1000 个 Tersors,这意味着我们创建的所有对象都没有被处理!如果我们看一下保持张量对象存活的链条(使用 objgraph.show_chain 创建),那么我们会发现:

enter image description here

有一个tensorflow图形对象,所有张量都在其中注册并保留,直到会话关闭。

到目前为止,这是理论,但是也不是:

#close session and free resources:
import keras
keras.backend.get_session().close()#free all resources

print("\n\n\n Counts after session.close():")
objgraph.show_most_common_types()

也不是这里提出的解决方案:

with tf.Graph().as_default(), tf.Session() as sess:
   for step in range(int(sys.argv[1])):
     foo(" ")

已经适用于当前的tensorflow版本。这可能是一个错误


简而言之:您在c++代码中没有做错任何事情,也没有内存泄漏需要负责。实际上,如果您从一个纯Python脚本中反复调用foo函数,您将看到完全相同的内存消耗。
所有创建的张量都在图形对象中注册,并且不会自动释放,您必须通过关闭后端会话来释放它们,但由于当前tensorflow版本1.4.0中存在错误,因此无法正常工作。

@TobiasHermann 有点尴尬,但我的分析相当错误。但现在我希望我弄对了... - ead
再次感谢。我刚刚测试了你的方法,但是无法通过它消除内存泄漏问题。现在的代码是这样的:main.cppscript.cpp。你有什么想法吗? - Tobias Hermann
@TobiasHermann 我认为在你的脚本中需要加入 import keras,否则 clear_session 会出问题,但是你没有正确地在 cpp 代码中处理它。我建议先检查你的 cpp 程序作为纯 Python 脚本是否正常运行。 - ead
我现在无法访问我的电脑,但稍后会查看。你能否在你的脚本中使用 graphobj (pip install graphobj) 来查看哪些对象是活着的以及为什么(链)? - ead
让我们在聊天中继续这个讨论。 (http://chat.stackoverflow.com/rooms/163308/discussion-between-ead-and-tobias-hermann) - ead
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接