将元素从一个出队移动到另一个时,C++ 使用两倍的内存
C++ uses twice the memory when moving elements from one dequeue to another
在我的项目中,我使用 pybind11 将 C++ 代码绑定到 Python。最近我不得不处理非常大的数据集 (70GB+),遇到需要将一个 std::deque
的数据拆分到多个 std::deque
之间。由于我的数据集很大,我希望拆分不会有太多内存开销。因此,我采用了一种流行 - 一种推动策略,通常应该确保满足我的要求。
理论上就是这样。实际上,我的进程被杀死了。所以我在过去的两天里苦苦挣扎,最终想出了下面这个证明问题的最小例子。
一般来说,最小的例子会在 deque
(~11GB) 中创建一堆数据,returns 到 Python,然后再次调用 C++
来移动元素.就那么简单。移动部分在执行器中完成。
有趣的是,如果我不使用 executor,内存使用情况符合预期,并且当 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();
}
test/__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
左右,太棒了!
- 当移动部分在执行器中完成时,程序占用双倍内存并崩溃。
引入虚拟内存限制 (~15Gb) 后,一切正常,这可能是最有趣的部分。
ulimit -Sv 15000000 && python3.5 test.py
>> Done!
.
当我们增加限制时程序崩溃(150Gb > 我的 RAM)。
ulimit -Sv 150000000 && python3.5 test.py
>> [1] 2573 killed python3.5 test.py
双端队列方法shrink_to_fit
的使用没有帮助(而且也不应该)
使用过的软件
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
备注
请注意,此示例仅用于演示问题。 asyncio
和 pybind
的使用是出现问题所必需的。
欢迎任何关于可能发生的事情的想法。
问题原来是由于数据在一个线程中创建,然后在另一个线程中释放。之所以如此,是因为 glibc (for reference see this) 中的 malloc arenas。它可以通过做很好地证明:
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)
这将占用 _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)
哪个没有受伤。
这个问题可以通过重写代码来解决,这样它就不会将元素从一个容器移动到另一个容器(我的情况),或者通过设置环境变量 export MALLOC_ARENA_MAX=1
来限制 malloc arenas 的数量1. 然而,这可能会涉及一些性能影响(有多个竞技场是有充分理由的)。
在我的项目中,我使用 pybind11 将 C++ 代码绑定到 Python。最近我不得不处理非常大的数据集 (70GB+),遇到需要将一个 std::deque
的数据拆分到多个 std::deque
之间。由于我的数据集很大,我希望拆分不会有太多内存开销。因此,我采用了一种流行 - 一种推动策略,通常应该确保满足我的要求。
理论上就是这样。实际上,我的进程被杀死了。所以我在过去的两天里苦苦挣扎,最终想出了下面这个证明问题的最小例子。
一般来说,最小的例子会在 deque
(~11GB) 中创建一堆数据,returns 到 Python,然后再次调用 C++
来移动元素.就那么简单。移动部分在执行器中完成。
有趣的是,如果我不使用 executor,内存使用情况符合预期,并且当 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();
}
test/__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
左右,太棒了! - 当移动部分在执行器中完成时,程序占用双倍内存并崩溃。
引入虚拟内存限制 (~15Gb) 后,一切正常,这可能是最有趣的部分。
ulimit -Sv 15000000 && python3.5 test.py
>>Done!
.当我们增加限制时程序崩溃(150Gb > 我的 RAM)。
ulimit -Sv 150000000 && python3.5 test.py
>>[1] 2573 killed python3.5 test.py
双端队列方法
shrink_to_fit
的使用没有帮助(而且也不应该)
使用过的软件
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
备注
请注意,此示例仅用于演示问题。 asyncio
和 pybind
的使用是出现问题所必需的。
欢迎任何关于可能发生的事情的想法。
问题原来是由于数据在一个线程中创建,然后在另一个线程中释放。之所以如此,是因为 glibc (for reference see this) 中的 malloc arenas。它可以通过做很好地证明:
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)
这将占用 _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)
哪个没有受伤。
这个问题可以通过重写代码来解决,这样它就不会将元素从一个容器移动到另一个容器(我的情况),或者通过设置环境变量 export MALLOC_ARENA_MAX=1
来限制 malloc arenas 的数量1. 然而,这可能会涉及一些性能影响(有多个竞技场是有充分理由的)。