2016-10-12 11 views
3

私のプロジェクトでは、pybind11を使用してC++コードをPythonにバインドします。最近、私は非常に大きなデータセット(70GB +)に対処しなければならず、std::dequeのデータを複数に分割する必要が生じましたstd::deque。私のデータセットは非常に大きいので、私はスプリットがメモリオーバーヘッドの多くを持っていないと思っています。だから私は一般的に私の要件が満たされることを確実にしなければならない1つのプッシュ戦略を行った。C++はデキューからデキューへ要素を移動するときに2倍のメモリを使用します

これはすべて理論です。実際には、私のプロセスが殺されました。だから私は過去2日間苦労し、最終的に問題を示す最小限の例を考え出しました。

一般的に、最小限の例では、deque(〜11GB)にデータの束を作成し、それをPythonに返してから、C++を呼び出して要素を移動します。そのような単純な。移動部分はエグゼキュータで実行されます。

私はエグゼキュータを使用しないと、メモリ使用量が予想どおりであり、ulimitによる仮想メモリの制限が課せられても、プログラムは実際にこれらの制限を守り、クラッシュしません。

test.py

from test import _test 
import asyncio 
import concurrent 

async def test_main(loop, executor): 
    numbers = _test.generate() 
    # moved_numbers = _test.move(numbers) # This works! 
    moved_numbers = await loop.run_in_executor(executor, _test.move, numbers) # This doesn't! 

if __name__ == '__main__': 
    loop = asyncio.get_event_loop() 
    executor = concurrent.futures.ThreadPoolExecutor(1) 

    task = loop.create_task(test_main(loop, executor)) 
    loop.run_until_complete(task) 

    executor.shutdown() 
    loop.close() 

TEST.CPP

#include <deque> 
#include <iostream> 
#include <pybind11/pybind11.h> 
#include <pybind11/stl.h> 

namespace py = pybind11; 

PYBIND11_MAKE_OPAQUE(std::deque<uint64_t>); 
PYBIND11_DECLARE_HOLDER_TYPE(T, std::shared_ptr<T>); 

template<class T> 
void py_bind_opaque_deque(py::module& m, const char* type_name) { 
    py::class_<std::deque<T>, std::shared_ptr<std::deque<T>>>(m, type_name) 
    .def(py::init<>()) 
    .def(py::init<size_t, T>()); 
} 

PYBIND11_PLUGIN(_test) { 
    namespace py = pybind11; 
    pybind11::module m("_test"); 
    py_bind_opaque_deque<uint64_t>(m, "NumbersDequeue"); 

    // Generate ~11Gb of data. 
    m.def("generate", []() { 
     std::deque<uint64_t> numbers; 
     for (uint64_t i = 0; i < 1500 * 1000000; ++i) { 
      numbers.push_back(i); 
     } 
     return numbers; 
    }); 

    // Move data from one dequeue to another. 
    m.def("move", [](std::deque<uint64_t>& numbers) { 
     std::deque<uint64_t> numbers_moved; 

     while (!numbers.empty()) { 
      numbers_moved.push_back(std::move(numbers.back())); 
      numbers.pop_back(); 
     } 
     std::cout << "Done!\n"; 
     return numbers_moved; 
    }); 

    return m.ptr(); 
} 

テスト/ __ init__.py

import warnings 
warnings.simplefilter("default") 

コンパイル

g++ -std=c++14 -O2 -march=native -fPIC -Iextern/pybind11 `python3.5-config --includes` `python3.5-config --ldflags` `python3.5-config --libs` -shared -o test/_test.so test.cpp 

観察:

  • 可動部がエグゼキュータによって行われていないので、我々はちょうどmoved_numbers = _test.move(numbers)を呼び出して、すべての作品予想通り、メモリ使用量がhtopのであったが周り11Gbのまま、 すばらしいです!。
  • 実行部分が実行プログラムで実行されると、プログラムはメモリを2倍にしてクラッシュします。
  • 仮想メモリの制限が導入されると(〜15Gb)、すべてがうまく動作します。これはおそらく最も興味深い部分です。

    ulimit -Sv 15000000 && python3.5 test.py >>Done!です。

  • 制限を大きくすると、プログラムがクラッシュします(150Gb> my RAM)。

    ulimit -Sv 150000000 && python3.5 test.py >>(すべきであり、またそれ)助けないshrink_to_fit両端キュー方式の[1] 2573 killed python3.5 test.py

  • 使用

使用するソフトウェア

Ubuntu 14.04 
gcc version 5.4.1 20160904 (Ubuntu 5.4.1-2ubuntu1~14.04) 
Python 3.5.2 
pybind11 latest release - v1.8.1 

この例は、単に問題を示すために作成されたものです。問題が発生するには、asynciopybindの使用が必要です。

何が起こっているかについてのアイデアは、大歓迎です。

答えて

1

この問題は、あるスレッドで作成されたデータが別のスレッドで作成され、次に別のスレッドで割り当て解除されたことが原因であることが判明しました。それはglibcのmallocアリーナのためです(for reference see this)。それはうまくやっによって証明することができる。_test.generate

executor = concurrent.futures.ThreadPoolExecutor(1) 

numbers = await loop.run_in_executor(executor, _test.generate) 
moved_numbers = await loop.run_in_executor(executor, _test.move, numbers) 

wound't

によって割り当てられた二回のメモリを取るだろう
executor1 = concurrent.futures.ThreadPoolExecutor(1) 
executor2 = concurrent.futures.ThreadPoolExecutor(1) 

numbers = await loop.run_in_executor(executor1, _test.generate) 
moved_numbers = await loop.run_in_executor(executor2, _test.move, numbers) 

この問題は、要素をあるコンテナから別のコンテナ(私の場合)に移動しないように、または環境変数export MALLOC_ARENA_MAX=1を設定することで、mallocアリーナの数を1に制限するようにコードを書き換えることによって解決できます。いくつかのパフォーマンスへの影響があります(複数のアリーナを持つ良い理由があります)。

関連する問題