Я постараюсь написать краткое резюме того, что я узнал, пытаясь выяснить, что происходит.
Примечание: этот ответ возможен благодаря @Lawrence - признателен!
Короче говоря
Это не имеет абсолютно никакого отношения ни к управлению памятью в Linux / ядре (виртуальной), ни к std::string
.
Все дело в памяти glibc
allocator - он просто выделяет огромные области памяти при первом (и не только, конечно) динамическом выделении (на поток) .
Подробности
MCVE
#include <thread>
#include <vector>
#include <chrono>
int main() {
std::vector<std::thread> workers;
for( unsigned i = 0; i < 192; ++i )
workers.emplace_back([]{
const auto x = std::make_unique<int>(rand());
while (true) std::this_thread::sleep_for(std::chrono::seconds(1));});
workers.back().join();
}
Пожалуйста, игнорируйте дерьмовую обработку потоков, я хотел, чтобы это было как можно короче.
Команды
Компиляция: g++ --std=c++14 -fno-inline -g3 -O0 -pthread test.cpp
.
Профиль: valgrind --tool=massif --pages-as-heap=[no|yes] ./a.out
Использование памяти
top
показывает 7'815'012
КиБ виртуальной памяти.
pmap
также показывает 7'815'016
КиБ виртуальной памяти.
Аналогичный результат показан massif
с pages-as-heap=yes
: 7'817'088
КиБ, см. Ниже.
С другой стороны, massif
с pages-as-heap=no
резко отличается - около 133 КиБ!
Вывод массива с pages-as-heap = yes
Использование памяти перед уничтожением программы:
100.00% (8,004,698,112B) (page allocation syscalls) mmap/mremap/brk, --alloc-fns, etc.
->99.78% (7,986,741,248B) 0x54E0679: mmap (mmap.c:34)
| ->46.11% (3,690,987,520B) 0x545C3CF: new_heap (arena.c:438)
| | ->46.11% (3,690,987,520B) 0x545CC1F: arena_get2.part.3 (arena.c:646)
| | ->46.11% (3,690,987,520B) 0x5463248: malloc (malloc.c:2911)
| | ->46.11% (3,690,987,520B) 0x4CB7E76: operator new(unsigned long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
| | ->46.11% (3,690,987,520B) 0x4026D0: std::_MakeUniq<int>::__single_object std::make_unique<int, int>(int&&) (unique_ptr.h:765)
| | ->46.11% (3,690,987,520B) 0x400EC5: main::{lambda()
| | ->46.11% (3,690,987,520B) 0x40225C: void std::_Bind_simple<main::{lambda()
| | ->46.11% (3,690,987,520B) 0x402194: std::_Bind_simple<main::{lambda()
| | ->46.11% (3,690,987,520B) 0x402102: std::thread::_Impl<std::_Bind_simple<main::{lambda()
| | ->46.11% (3,690,987,520B) 0x4CE2C7E: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
| | ->46.11% (3,690,987,520B) 0x51C96B8: start_thread (pthread_create.c:333)
| | ->46.11% (3,690,987,520B) 0x54E63DB: clone (clone.S:109)
| |
| ->33.53% (2,684,354,560B) 0x545C35B: new_heap (arena.c:427)
| | ->33.53% (2,684,354,560B) 0x545CC1F: arena_get2.part.3 (arena.c:646)
| | ->33.53% (2,684,354,560B) 0x5463248: malloc (malloc.c:2911)
| | ->33.53% (2,684,354,560B) 0x4CB7E76: operator new(unsigned long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
| | ->33.53% (2,684,354,560B) 0x4026D0: std::_MakeUniq<int>::__single_object std::make_unique<int, int>(int&&) (unique_ptr.h:765)
| | ->33.53% (2,684,354,560B) 0x400EC5: main::{lambda()
| | ->33.53% (2,684,354,560B) 0x40225C: void std::_Bind_simple<main::{lambda()
| | ->33.53% (2,684,354,560B) 0x402194: std::_Bind_simple<main::{lambda()
| | ->33.53% (2,684,354,560B) 0x402102: std::thread::_Impl<std::_Bind_simple<main::{lambda()
| | ->33.53% (2,684,354,560B) 0x4CE2C7E: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
| | ->33.53% (2,684,354,560B) 0x51C96B8: start_thread (pthread_create.c:333)
| | ->33.53% (2,684,354,560B) 0x54E63DB: clone (clone.S:109)
| |
| ->20.13% (1,611,399,168B) 0x51CA1D4: pthread_create@@GLIBC_2.2.5 (allocatestack.c:513)
| ->20.13% (1,611,399,168B) 0x4CE2DC1: std::thread::_M_start_thread(std::shared_ptr<std::thread::_Impl_base>, void (*)()) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
| ->20.13% (1,611,399,168B) 0x4CE2ECB: std::thread::_M_start_thread(std::shared_ptr<std::thread::_Impl_base>) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
| ->20.13% (1,611,399,168B) 0x40139A: std::thread::thread<main::{lambda()
| ->20.13% (1,611,399,168B) 0x4012AE: _ZN9__gnu_cxx13new_allocatorISt6threadE9constructIS1_IZ4mainEUlvE_EEEvPT_DpOT0_ (new_allocator.h:120)
| ->20.13% (1,611,399,168B) 0x401075: _ZNSt16allocator_traitsISaISt6threadEE9constructIS0_IZ4mainEUlvE_EEEvRS1_PT_DpOT0_ (alloc_traits.h:527)
| ->19.19% (1,535,864,832B) 0x401009: void std::vector<std::thread, std::allocator<std::thread> >::emplace_back<main::{lambda()
| | ->19.19% (1,535,864,832B) 0x400F47: main (test.cpp:10)
| |
| ->00.94% (75,534,336B) in 1+ places, all below ms_print's threshold (01.00%)
|
->00.22% (17,956,864B) in 1+ places, all below ms_print's threshold (01.00%)
Вывод массива с pages-as-heap = no
Использование памяти перед уничтожением программы:
--------------------------------------------------------------------------------
n time(i) total(B) useful-heap(B) extra-heap(B) stacks(B)
--------------------------------------------------------------------------------
68 2,793,125 143,280 136,676 6,604 0
95.39% (136,676B) (heap allocation functions) malloc/new/new[], --alloc-fns, etc.
->50.74% (72,704B) 0x4EBAEFE: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
| ->50.74% (72,704B) 0x40106B8: call_init.part.0 (dl-init.c:72)
| ->50.74% (72,704B) 0x40107C9: _dl_init (dl-init.c:30)
| ->50.74% (72,704B) 0x4000C68: ??? (in /lib/x86_64-linux-gnu/ld-2.23.so)
|
->36.58% (52,416B) 0x40138A3: _dl_allocate_tls (dl-tls.c:322)
| ->36.58% (52,416B) 0x53D126D: pthread_create@@GLIBC_2.2.5 (allocatestack.c:588)
| ->36.58% (52,416B) 0x4EE9DC1: std::thread::_M_start_thread(std::shared_ptr<std::thread::_Impl_base>, void (*)()) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
| ->36.58% (52,416B) 0x4EE9ECB: std::thread::_M_start_thread(std::shared_ptr<std::thread::_Impl_base>) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
| ->36.58% (52,416B) 0x40139A: std::thread::thread<main::{lambda()
| ->36.58% (52,416B) 0x4012AE: _ZN9__gnu_cxx13new_allocatorISt6threadE9constructIS1_IZ4mainEUlvE_EEEvPT_DpOT0_ (new_allocator.h:120)
| ->36.58% (52,416B) 0x401075: _ZNSt16allocator_traitsISaISt6threadEE9constructIS0_IZ4mainEUlvE_EEEvRS1_PT_DpOT0_ (alloc_traits.h:527)
| ->34.77% (49,824B) 0x401009: void std::vector<std::thread, std::allocator<std::thread> >::emplace_back<main::{lambda()
| | ->34.77% (49,824B) 0x400F47: main (test.cpp:10)
| |
| ->01.81% (2,592B) 0x4010FF: void std::vector<std::thread, std::allocator<std::thread> >::_M_emplace_back_aux<main::{lambda()
| ->01.81% (2,592B) 0x40103D: void std::vector<std::thread, std::allocator<std::thread> >::emplace_back<main::{lambda()
| ->01.81% (2,592B) 0x400F47: main (test.cpp:10)
|
->06.13% (8,784B) 0x401B4B: __gnu_cxx::new_allocator<std::_Sp_counted_ptr_inplace<std::thread::_Impl<std::_Bind_simple<main::{lambda()
| ->06.13% (8,784B) 0x401A60: std::allocator_traits<std::allocator<std::_Sp_counted_ptr_inplace<std::thread::_Impl<std::_Bind_simple<main::{lambda()
| ->06.13% (8,784B) 0x40194D: std::__shared_count<(__gnu_cxx::_Lock_policy)2>::__shared_count<std::thread::_Impl<std::_Bind_simple<main::{lambda()
| ->06.13% (8,784B) 0x401894: std::__shared_ptr<std::thread::_Impl<std::_Bind_simple<main::{lambda()
| ->06.13% (8,784B) 0x40183A: std::shared_ptr<std::thread::_Impl<std::_Bind_simple<main::{lambda()
| ->06.13% (8,784B) 0x4017C7: std::shared_ptr<std::thread::_Impl<std::_Bind_simple<main::{lambda()
| ->06.13% (8,784B) 0x4016AB: std::shared_ptr<std::thread::_Impl<std::_Bind_simple<main::{lambda()
| ->06.13% (8,784B) 0x40155E: std::shared_ptr<std::thread::_Impl<std::_Bind_simple<main::{lambda()
| ->06.13% (8,784B) 0x401374: std::thread::thread<main::{lambda()
| ->06.13% (8,784B) 0x4012AE: _ZN9__gnu_cxx13new_allocatorISt6threadE9constructIS1_IZ4mainEUlvE_EEEvPT_DpOT0_ (new_allocator.h:120)
| ->06.13% (8,784B) 0x401075: _ZNSt16allocator_traitsISaISt6threadEE9constructIS0_IZ4mainEUlvE_EEEvRS1_PT_DpOT0_ (alloc_traits.h:527)
| ->05.83% (8,352B) 0x401009: void std::vector<std::thread, std::allocator<std::thread> >::emplace_back<main::{lambda()
| | ->05.83% (8,352B) 0x400F47: main (test.cpp:10)
| |
| ->00.30% (432B) in 1+ places, all below ms_print's threshold (01.00%)
|
->01.43% (2,048B) 0x403432: __gnu_cxx::new_allocator<std::thread>::allocate(unsigned long, void const*) (new_allocator.h:104)
| ->01.43% (2,048B) 0x4032CF: std::allocator_traits<std::allocator<std::thread> >::allocate(std::allocator<std::thread>&, unsigned long) (alloc_traits.h:488)
| ->01.43% (2,048B) 0x4030B8: std::_Vector_base<std::thread, std::allocator<std::thread> >::_M_allocate(unsigned long) (stl_vector.h:170)
| ->01.43% (2,048B) 0x4010B6: void std::vector<std::thread, std::allocator<std::thread> >::_M_emplace_back_aux<main::{lambda()
| ->01.43% (2,048B) 0x40103D: void std::vector<std::thread, std::allocator<std::thread> >::emplace_back<main::{lambda()
| ->01.43% (2,048B) 0x400F47: main (test.cpp:10)
|
->00.51% (724B) in 1+ places, all below ms_print's threshold (01.00%)
Что за урод происходит?
pages-as-heap = no
С pages-as-heap=no
все выглядит разумно - давайте не будем это проверять.Как и ожидалось, все заканчивается malloc/new/new[]
, а использование памяти достаточно мало, чтобы не беспокоить нас - это высокоуровневые выделения.
pages-as-heap = yes
Но посмотритеpages-as-heap=yes
?~ 8 ГБ виртуальной памяти с этим простым кодом?
Давайте проверим следы стека.
pthread_create
Давайте начнем с более простого: того, который заканчивается pthread_create
.
massif
отчетов 1,611,399,168
байт выделенной памяти - это ровно 192 * 8'196 КиБ, что означает - 192 потока * 8MiB, , что является максимальным размером стека потока по умолчанию в Linux .
Обратите внимание , что 8'196 КиБ не совсем 8 МБ (8'192 КиБ).Я не знаю, откуда взялась эта разница, но на данный момент она незначительна.
std::make_unique<int>
Хорошо, давайте теперь посмотрим на другие два стека... подожди, они точно такие же?Да, документация massif
объясняет это, я не совсем понял это, но это также не важно.Они показывают точно такой же стек.Давайте объединим результаты и рассмотрим их вместе.
Использование памяти из этих двух стеков вместе составляет 6'375'342'080
байтов, и все они вызваны нашим простым std::make_unique<int>
!
Давайте возьмемшаг назад: если мы выполним тот же эксперимент, но с простым потоком, мы увидим, что это int
выделение приводит к выделению 67'108'864
байтов памяти, что в точности равно 64 МБ.Что происходит ??
Все сводится к реализации malloc
(как мы все знаем, что new/new[]
внутренне реализован с malloc
.. по умолчанию).
malloc
внутренне использует распределитель памяти, называемый ptmalloc2
- распределитель памяти по умолчанию в Linux, который поддерживает потоки.
Проще говоря, этот распределитель имеет дело со следующими терминами:
per thread arena
: огромная область памяти;обычно на поток, по соображениям производительности;не все программные потоки имеют свои арены для потоков , обычно это зависит от количества аппаратных потоков (и других деталей, я полагаю); heap
: arena
s разделены на кучи; chunks
: heap
s разделены на меньшие области памяти, называемые chunks
.
Есть много деталейОб этих вещах мы опубликуем несколько интересных ссылок чуть позже, хотя этого должно быть достаточно для того, чтобы читатель мог провести собственное исследование - это действительно низкоуровневые и глубокие вещи, связанные с управлением памятью в C ++.
Итак, давайте вернемся к нашему тесту с одним потоком - выделил 64 МБ для одного int
??Давайте снова посмотрим на трассировку стека и сконцентрируемся в конце:
mmap (mmap.c:34)
new_heap (arena.c:438)
arena_get2.part.3 (arena.c:646)
malloc (malloc.c:2911)
Сюрприз, сюрприз: malloc
вызывает arena_get2
, что вызывает new_heap
, что приводит нас к mmap
(mmap
и brk
- системные вызовы низкого уровня, используемые для выделения памяти в Linux).И это, как сообщается, выделяет ровно 64 МБ памяти.
Хорошо, теперь давайте вернемся к нашему первоначальному примеру с 192 потоками и нашим огромным числом 6'375'342'080
- это точно 95 *64 МиБ!
Почему именно 95 - я не могу сказать, я перестал копать, но тот факт, что большое число делится на 64 МиБ, был достаточно хорош для меня.
Вы можете копать глубже, если это необходимо.
Полезные ссылки
Действительно классная пояснительная статья: Понимание glibc malloc , от sploitfun
Более официальная / официальная документация: Распределитель GNU
Вопрос об обмене классным стеком: Как работает glibc malloc
Другие:
Если некоторые из этих ссылок не работают на момент прочтения этого поста, найти подобные статьи будет довольно легко.Эта тема очень популярна, если вы знаете, что искать и как.
Спасибо
Я надеюсь, что эти наблюдения дают хорошее общее описание всей картины, а также дают достаточно пищи для дальнейших расширенных исследований.
Не стесняйтесь комментировать / (предложить) изменить / исправить / расширить / и т. д.