Как управлять стратегией распределения памяти в коде сторонней библиотеки?

Предыдущий заголовок: "Должен ли я заменять глобальные операторы new и delete, чтобы изменить стратегию распределения памяти в стороннем коде?"

Рассказ: Нам нужно заменить технологию выделения памяти в сторонней библиотеке, не изменяя ее исходный код.

Длинная история:

Рассмотрим приложение с привязкой к памяти, которое делает огромные динамические распределения (возможно, почти все доступную системную память). Мы используем специализированные распределители и используем их везде (shared_ptr, контейнеры и т.д.). У нас есть полный контроль и власть над каждым байтом памяти, выделенным в нашем приложении.

Кроме того, нам нужно связать с сторонней вспомогательной библиотекой. Этот противный парень делает выделение стандартным образом, используя операторы по умолчанию new, new[], delete и delete[] или malloc или что-то еще нестандартное (пусть обобщают и говорят, что мы не знаем, как эта библиотека управляет распределением кучи).

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

Мы не можем (или не хотим) изменять исходный код библиотеки.

Первая попытка:

У нас никогда не было таких нечестивых "хаков" в сборках релизов. Первый тест с переопределяющим оператором new работает отлично, за исключением того, что:

  • мы не знаем, что ждет нас в будущем (и это ужасно)
  • наши пользователи (и даже наши распределители) теперь должны выделять то же самое, что мы делаем

Вопросы:

  • Есть ли способы перехвата этих распределений без перегрузки глобальных операторов? (локальные перехватчики только для lib?)
  • ... и если мы не знаем, что именно он использует: malloc или new?
  • Является ли этот список сигнатур законченным? (и нет других вещей, которые мы должны реализовать):

    void* operator new (std::size_t size) throw (std::bad_alloc);
    void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) throw();
    void* operator new (std::size_t size, void* ptr) throw();
    void* operator new[] (std::size_t size) throw (std::bad_alloc);
    void* operator new[] (std::size_t size, const std::nothrow_t& nothrow_value) throw();
    void* operator new[] (std::size_t size, void* ptr) throw();
    void operator delete (void* ptr) throw();
    void operator delete (void* ptr, const std::nothrow_t& nothrow_constant) throw();
    void operator delete (void* ptr, void* voidptr2) throw();
    void operator delete[] (void* ptr) throw();
    void operator delete[] (void* ptr, const std::nothrow_t& nothrow_constant) throw();
    void operator delete[] (void* ptr, void* voidptr2) throw();
  • Что-то другое, если эта библиотека динамична?

Изменить # 1

Кросс-платформенное решение предпочтительнее, если это возможно (похоже, не очень возможно). Если нет, наши основные платформы:

  • Windows x86/x64 (msvc 10)
  • Linux x86/x64 (gcc 4.6)

Изменить # 2

Прошло почти 2 года, несколько версий ОС и компиляторов эволюционировали, поэтому мне интересно, есть ли что-то новое и неисследованное в этой области? Любые стандартные предложения? OS-специфика? Хаки? Как вы пишете приложения, жаждущие памяти? Поделитесь своим опытом.

3 ответа

Ух, мое сочувствие. Это будет сильно зависеть от вашего компилятора, вашего libc и т.д. Некоторые стратегии с резиновыми дорожками, которые "в прошлом" работали в разной степени для нас (/me скобки для downvotes):

  • Перегруженные версии operator new/operator delete - хотя обратите внимание, что некоторые компиляторы придирчивы к тому, чтобы не иметь спецификаций throw(), некоторые действительно хотят их, некоторые хотят их для новых, но не для удаления и т.д. (у меня есть гигантский платформенный блок #if/#elif для всех платформ 4+, над которыми мы сейчас работаем).
  • Также стоит отметить: вы обычно можете игнорировать версии размещения, они не выделяются.
  • Посмотрите __malloc_hook и друзей - обратите внимание, что они устарели и имеют условия гонки нитей - но они хороши в этом new/delete обычно реализуются с точки зрения malloc (но не всегда).
  • Предоставление замены malloc, calloc, realloc и free и получение ваших ссылок в правильном порядке, чтобы произошли переопределения (это то, что gcc рекомендует в наши дни, хотя я были ситуации, когда это было невозможно сделать, и мне пришлось использовать устаревшие __malloc_hook) - опять же, new и delete имеют тенденцию реализовываться в терминах этих, но не всегда.
  • Избегайте всех стандартных методов распределения (operator new, malloc и т.д.) в "нашем коде" и вместо этого используйте нестандартные функции - не очень просто с существующей кодовой базой.
  • Отслеживание автора библиотеки и предоставление вежливого запроса или патча savage beating, чтобы изменить их библиотеку, чтобы вы могли указать другой распределитель (это может быть быстрее, чем делать это самостоятельно) - я думаю это привело к кардинальному правилу "клиент всегда указывает распределитель или выполняет распределение" с любыми библиотеками, которые я пишу.

Обратите внимание, что это не ответ с точки зрения того, что говорят стандарты, только мой опыт. Я работал с более чем несколькими ошибками/сломанными компиляторами и реализациями libc в прошлом, поэтому YMMV. У меня также есть роскошь работать над довольно "запечатанными системами" и не беспокоиться о переносимости для любого конкретного приложения.

Относительно динамических библиотек: я сейчас немного в этом плане, наше "приложение" загружается как динамическое .so, и мы должны быть очень осторожны, чтобы передать любые запросы delete/free обратно в распределитель по умолчанию, если они не пришли от нас. Нынешнее решение состоит в том, чтобы просто оторвать наши распределения до определенной области: если мы получим удаление/освобождение в пределах этого диапазона адресов, мы отправим наш обработчик, в противном случае вернемся к умолчанию... Я даже играл с (ужасы ) идея проверки адреса вызывающего абонента, чтобы узнать, находится ли он в нашем адресном пространстве. (Вероятность того, что бум растет с такими хаками, хотя.)

Это может быть полезной стратегией, даже если вы являетесь лидером процесса, и вы используете внешнюю библиотеку: тег или ограничиваете или иным образом идентифицируете свои собственные allocs каким-то образом (даже если вы придерживаетесь списка выделений, которые вы знаете о), а затем передать любые неизвестные. Однако все это имеет уродливые побочные эффекты и ограничения.

(Ждем других ответов!)


Без возможности изменить исходный код библиотеки - или, лучше, иметь возможность влиять на автора библиотеки, чтобы изменить его - я бы сказал, что вам не повезло.

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

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

К сожалению, это противоречит вашему требованию не изменять библиотеку - я скептически отношусь к возможности удовлетворить это, особенно в рамках ограничений, которые вы наметили (из-за жажды, размещенных на windows/linux и т.д.).


Невозможно выполнить выделение в этой библиотеке классов, но вы можете использовать новое размещение для размещения классов из этой сторонней библиотеки, то есть вы можете выделить память и иметь конструкторы тех классов, которые вызываются в выделенной памяти. Таким образом, даже если класс имеет свой собственный новый оператор, который он не получил бы вызванным .Howvwer, внутри распределений памяти операций классов с неэкспонированными внутренними классами или примитивами будет выполняться с использованием схемы распределения сторонней библиотеки; которые не могут быть изменены, если библиотека сторонних разработчиков не позволяет указать распределитель, например, stl-контейнеры

licensed under cc by-sa 3.0 with attribution.