Pull to refresh

Использование альтернативного аллокатора памяти в проекте на C/C++

Reading time 4 min
Views 18K
Эта статья написана прежде всего для программистов C/C++, использующих в своей работе Visual Studio 2013. Поскольку я, как говорится, totally windows guy, то я не могу оценить полезность этой статьи для программистов, не использующих эту среду в своей работе. Итак.

Не секрет, что стандартный аллокатор new/delete/malloc/free в языке C/C++ не блещет быстродействием. Конечно, всё зависит от реализации, но, если говорить об оной от компании Microsoft, то это факт. Кроме того, стандартная реализация аллокатора обладает еще одним фатальным недостатком — фрагментацией памяти. Если в вашей программе происходят частые выделения/освобождения памяти, вы можете обнаружить, что спустя несколько часов работы ваша программа упала по причине нехватки памяти, хотя свободной памяти еще достаточно — просто в результате фрагментации в пуле аллокатора не осталось свободного участка достаточного размера. (Это, кстати, абсолютно реальный случай, который произошел на одном из проектов, в котором я принимал непосредственное участие.)

К счастью, существуют аллокаторы, которые лишены обоих этих недостатков. В свое время я попробовал dlmalloc и с тех пор всегда использую его в своих проектах.

Хочу поделиться с вами способом подключения dlmalloc к проекту на Visual Studio C/C++.
Способ, который я использую, примечателен тем, что позволяет использовать альтернативный аллокатор абсолютно для всех аллокаций, которые только могут случиться в вашей программе. Да, простой способ (т.е. замена вызовов malloc на dlmalloc) не достигает этого эффекта. Например, вы подключили стороннюю библиотеку, которая выделят память с помощью malloc. Более того, некоторые вызовы стандартных функций из stdlib также выделяют память функцией malloc и у вас нет возможности этому помешать… Или есть? Есть.

Суть способа


Суть способа в том, чтобы заставить линковщик использовать вашу реализацию malloc/free/new/delete вместо стандартной. Но как это сделать? Когда я только начал исследовать этот вопрос, моей первой попыткой была достаточно глупая идея: пропатчить в runtime тело malloc/free в памяти, поместив туда безусловный jmp на мой код. Надо ли объяснять, почему эта идея глупа? Хотя все работало, но радости этот способ не приносил. В итоге я пришел к другому решению, а именно — запретить линковщику вообще использовать стандартную библиотеку libcmt, в которой и находится стандартный аллокатор. Но и этот способ обладал существенным недостатком, а именно, в этой библиотеке достаточно много других полезных и не очень функций, написать к которым заглушки было категорически невозможно.
Тогда я стал исследовать возможность взять стандартную библиотеку (буквально файл libcmt.lib) и выкинуть из него всё лишние. Оказалось, что это возможно и в итоге этот способ я и использую.

Небольшое отступление
Я говорю о файле libcmt.lib, однако вы должны понимать, что всё тоже самое справедливо и для libc.lib. Объяснение разницы между этими библиотеками выходит за рамки этой статьи.

Технические подробности


Сначала выполним команду:

lib.exe /LIST libcmt.lib

На выходе получим список obj файлов, которые эта библиотека содержит. Для libcmt.lib из Visual Studio 2013 этот список выглядит примерно так:

f:\binaries\Intermediate\vctools\crt_bld\SELF_X86\crt\prebuild\INTEL\mt_lib\chandler4.obj
f:\binaries\Intermediate\vctools\crt_bld\SELF_X86\crt\prebuild\INTEL\mt_lib\chandler4gs.obj
f:\binaries\Intermediate\vctools\crt_bld\SELF_X86\crt\prebuild\INTEL\mt_lib\chkesp.obj
f:\binaries\Intermediate\vctools\crt_bld\SELF_X86\crt\prebuild\INTEL\mt_lib\eh3valid.obj
f:\binaries\Intermediate\vctools\crt_bld\SELF_X86\crt\prebuild\INTEL\mt_lib\exsup.obj
f:\binaries\Intermediate\vctools\crt_bld\SELF_X86\crt\prebuild\INTEL\mt_lib\exsup2.obj
... (и так далее)

К счастью, практически все функции по работе с памятью находятся в отдельных obj файлах, что, собственно, и делает этот способ возможным.
Т.е. нам остается вырезать из тела библиотеки все ненужные obj файлы.
Утилита lib.exe с ключем /remove как раз делает то, что нам нужно.

Реализация


Собственно, исходные коды я выложил на гитхабе.

Если у вас уже установлена Visual Studio 2013, достаточно запустить make_libcmt_nomem.cmd, который выполнит всю работу и создаст обрезанный файл libcmt_nomem.lib, который можно подключать вместо полновесного libcmt.

В своей работе скрипт использует unix утилиту grep. Если у вас не установлены UnixUtils, настоятельно рекомендую это сделать (например отсюда).

Но это еще не всё. От стандартного аллокатора мы избавились. Но беда в том, что заодно мы избавились и от некоторой стандартной функциональности, которая, увы, неотделима от аллокатора. Поэтому мной были написаны необходимые заглушки, которые вы можете найти в файле include/crtfunc.h (там же, на гитхабе).

Способ применения


  1. Получаем обрезанную версию стандартной библиотеки с помощью скрипта make_libcmt_nomem.cmd и кладем ее в доступное для линковщика место;
  2. Отключаем в проекте использование стандартной библиотеки libcmt (Ignore Specific Default Libraries" «libcmt» в опциях линкера Configuration Properties->Linker->Input);
  3. В любом c++ файле в проекте делаем "#include crtfunc.h" из исходников;
  4. Подключаем dlmalloc к проекту.

Я не расписываю подробно каждый пункт, поскольку, если вы прочитали эту статью и поняли ее, подробности вам и не требуются. Единственный момент: подключать crtfunc.h следует именно в C++ (не C) файл. Если ваш проект написан на C, вам следует добавить к проекту пустой .cpp файл и включить в него crtfunc.h. Впрочем, никто не запрещает вам взять в руки напильник.

PS. На самом деле, не dlmalloc'ом единым. Существуют и другие, весьма достойные аллокаторы. Исходные файлы рассчитаны на dlmalloc, но это не принципиально. Минимальным вмешательством в crtfunc.h можно добиться использования любого другого аллокатора.

После выхода Visual Studio 2015 всё вышенаписанное утратило актуальность
Tags:
Hubs:
+13
Comments 40
Comments Comments 40

Articles