Подтвердить что ты не робот

Как библиотеки С++ позволяют настраивать распределители?

В C просто для библиотеки просто позволить пользователю настраивать выделение памяти с помощью глобальных указателей функций на функцию, которая должна вести себя аналогично malloc() и функции, которая должна вести себя аналогично free(). SQLite, например, использует этот подход.

С++ усложняет ситуацию, потому что распределение и инициализация обычно сливаются. По сути, мы хотим получить поведение переопределенных operator new и operator delete только для библиотеки, но нет способа сделать это (я достаточно уверен, но не совсем на 100%).

Как это сделать в С++?

Здесь первый удар по чему-то, что реплицирует некоторую семантику выражений new с помощью функции Lib::make<T>.

Я не знаю, насколько это полезно, но просто для удовольствия, здесь более сложная версия, которая также пытается реплицировать семантику выражений new[].

Это целевой вопрос, поэтому я не обязательно ищу проверку кода. Если есть лучший способ сделать это, просто скажите это и проигнорируйте ссылки.

( "распределителем" я имею в виду только то, что выделяет память. Я не имею в виду концепцию распределителя STL или даже обязательно выделяю память для контейнеров.)


Почему это может быть желательно:

Здесь сообщение в блоге от разработчика Mozilla, в котором утверждается, что библиотеки должны это делать. Он дает несколько примеров библиотек C, которые позволяют пользователю библиотеки настраивать распределение для библиотеки. Я проверил исходный код для одного из примеров SQLite и увидел, что эта функция также используется внутри для тестирования с помощью впрыска. Я не пишу ничего, что должно быть как пуленепробиваемое, как SQLite, но оно все же кажется разумной идеей. Если ничего другого, он позволяет коду клиента определить: "Какая библиотека забивает мою память и когда?".

4b9b3361

Ответ 1

Простой ответ: не используйте С++. Извините, шутка.

Но если вы хотите использовать этот абсолютный контроль над управлением памятью на С++, через границы библиотек и модулей и полностью обобщенным образом, вы можете быть в ужасном состоянии. Я бы предложил большинству искать причины, чтобы не делать этого больше, чем способы сделать это.

В течение многих лет (на самом деле десятилетия) я пережил множество итераций той же основной идеи, от попыток наивно перегрузить оператор new/new []/delete/delete [] на глобальном уровне в решения на основе компоновщика платформенные решения, и я нахожусь в нужной точке, в которой вы сейчас находитесь: у меня есть система, которая позволяет мне видеть объем выделенной памяти для каждого плагина. Но я не достиг этой точки с помощью обобщенного способа, который вы желаете (и меня тоже, изначально).

С++ немного усложняет ситуацию, поскольку распределение и инициализация обычно плавлены.

Я бы предложил небольшой поворот к этому утверждению: С++ усложняет ситуацию, поскольку инициализация и распределение обычно сливаются. Все, что я сделал, это обменный порядок здесь, но самая сложная часть - это не то, что выделение хочет инициализировать, а потому, что инициализация часто хочет выделить.

Возьмите этот базовый пример:

struct Foo
{
    std::vector<Bar> stuff;
};

В этом случае мы можем легко выделить Foo через специализированный распределитель памяти:

void* mem = custom_malloc(sizeof(Foo));
Foo* foo = new(foo_mem) Foo;
...
foo->~Foo();
custom_free(foo);

... и, конечно же, мы можем обернуть это все, что нам нравится, соответствовать RAII, достичь безопасности исключений и т.д.

Кроме того, проблема каскадируется. Этот член stuff, использующий std::vector, захочет использовать std::allocator, и теперь у нас есть вторая проблема для решения. Мы могли бы использовать экземпляр шаблона std::vector с помощью нашего собственного распределителя, и если вам нужна информация о времени выполнения, переданная распределителю, вы можете переопределить конструкторы Foo для передачи этой информации вместе с распределителем в векторный конструктор.

Но как насчет Bar? Его конструктор может также захотеть выделить память для различных разрозненных объектов, поэтому проблема каскадирует и каскады и каскады.

Учитывая сложность этой проблемы и альтернативные, обобщенные решения, которые я пробовал, и горе, связанные с переносом, я решил полностью деформированный, несколько прагматичный подход.

Решение, на котором я остановился, - это эффективно переработать всю стандартную библиотеку C и С++. Отвратительно, я знаю, но у меня было немного больше оправдания, чтобы сделать это в моем случае. Продукт, над которым я работаю, представляет собой набор для разработки двигателей и программного обеспечения, предназначенный для того, чтобы позволить людям писать плагины для него с использованием любого компилятора, исполняемого файла C, стандартной реализации библиотеки С++ и желаемых параметров сборки. Чтобы позволить вещам, таким как векторы или наборы или карты, проходить через эти центральные API-интерфейсы по совместимому с ABI образом, необходимо скопировать наши собственные стандартные совместимые контейнеры в дополнение к множеству стандартных функций C.

Вся реализация этого devkit затем вращается вокруг этих функций распределения:

EP_API void* ep_malloc(int lib_id, int size);
EP_API void ep_free(int lib_id, void* mem);

... и весь SDK вращается вокруг этих двух, включая пулы памяти и "суб-распределители".

Для сторонних библиотек вне нашего контроля мы просто SOL. Некоторые из этих библиотек имеют одинаково амбициозные вещи, которые они хотят делать с управлением памятью, и попытаться переопределить это приведет к разным столкновениям и откроет всевозможные банки червей. Существуют также очень низкоуровневые драйверы при использовании таких вещей, как OGL, которые хотят выделить много системной памяти, и мы ничего не можем с этим поделать.

Тем не менее, я нашел, что это решение работает достаточно хорошо, чтобы ответить на основной вопрос: "Кто/что выкачивает всю эту память?" очень быстро: вопрос, который часто бывает гораздо сложнее ответить, чем аналогичный, связанный с тактовыми циклами (для которого мы можем просто запустить любой профайлер). Он применяется только для кода под нашим контролем, используя этот SDK, но мы можем получить очень тщательное разбиение памяти с использованием этой системы на основе каждого модуля. Мы также можем установить поверхностные ограничения на использование памяти, чтобы убедиться, что ошибки в памяти фактически обрабатываются правильно, не пытаясь исчерпать все непрерывные страницы, доступные в системе.

Итак, в моем случае эта проблема была решена с помощью политики: путем создания единого стандарта кодирования и соответствующей ему центральной библиотеки, которая использовалась на всей кодовой базе (и третьи стороны писали плагины для нашей системы). Вероятно, это не тот ответ, который вы ищете, но это оказалось самым практичным решением, которое мы нашли.