x86 64 — Как я могу эмулировать кадр стека в C ++?

Я пишу контейнер, который использует alloca внутренне для размещения данных в стеке. Риски использования alloca в сторону, Предположим, что я должен использовать его для домена, в котором я нахожусь (это отчасти alloca и частично для изучения возможных реализаций динамически измеренных стековых контейнеров).

Согласно man страница для alloca (выделение мое):

Функция alloca () распределяет байты размера пространства в кадре стека вызывающей стороны. Это временное пространство автоматически освобождается, когда функция, вызвавшая alloca (), возвращается к своему вызывающему.

Используя функции, специфичные для реализации, мне удалось принудительно включить встраивание таким образом, чтобы стек вызывающих абонентов использовался для этой «области видимости» на уровне функций.

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

for(auto iteration : range(0, 10000)) {
// the ctor parameter is the number of
// instances of T to allocate on the stack,
// it's not normally known at compile-time
my_container<T> instance(32);
}

Не зная подробностей реализации этого контейнера, можно ожидать, что любая память, которую он выделяет, будет свободной, когда instance выходит за рамки. Это не тот случай, и он может привести к переполнению стека / высокому использованию памяти во время выполнения функции включения.

Один подход, который пришел в голову, состоял в том, чтобы явно освободить память в деструкторе. Если не считать реверс-инжиниринг полученной сборки, я пока не нашел способа сделать это (см. Также этот).

Единственный другой подход, о котором я подумал, — это иметь максимальный размер, указанный во время компиляции, использовать его для выделения буфера фиксированного размера, иметь реальный размер, указанный во время выполнения, и использовать буфер фиксированного размера для внутреннего использования. Проблема в том, что это потенциально очень расточительно (предположим, ваш максимум был 256 байтов на контейнер, но в большинстве случаев вам требовалось только 32).

Отсюда этот вопрос; Я хочу найти способ предоставить семантику области действия пользователям этого контейнера. Непереносимость — это хорошо, если она надежна на платформе и нацеливается (например, хорошо подходит некоторое документированное расширение компилятора, которое работает только для x86_64).

Я ценю это может быть XY проблема, Итак, позвольте мне четко сформулировать мои цели:

  • Я пишу контейнер, который должен всегда выделить свою память в стеке (насколько мне известно, это исключает C VLA).
  • Размер контейнера неизвестен во время компиляции.
  • Я хотел бы сохранить семантику памяти, как если бы она была проведена std::unique_ptr внутри контейнера.
  • Хотя контейнер должен иметь C ++ API, использование расширений компилятора из C вполне подойдет.
  • На данный момент код должен работать только на x86_64.
  • Целевая операционная система может быть на основе Linux или Windows, она не должна работать на обоих.

1

Решение

Я пишу контейнер, который всегда должен выделять свою память в стеке (насколько мне известно, это исключает C VLA).

Нормальная реализация C VLA в большинстве компиляторов находится в стеке. Конечно, ISO C ++ ничего не говорит о как автоматическое хранение реализовано внутри, но оно (почти?) универсально для реализаций C на обычных машинах (у которых есть стек вызовов + данных), чтобы использовать это для всего автоматического хранения, включая VLA.

Если ваш VLA слишком велик, вы получаете переполнение стека, а не откат к malloc / free,

Ни C, ни C ++ не указывают alloca; он доступен только для реализаций, которые имеют стек, подобный «обычным» машинам, то есть тем же самым машинам, где вы можете ожидать, что VLA будут делать то, что вы хотите.

Все эти условия выполняются для всех основных компиляторов на x86-64 (за исключением того, что MSVC не поддерживает VLA).


Если у вас есть компилятор C ++, который поддерживает C99 VLA (например, GNU C ++), Умные компиляторы могут повторно использовать одну и ту же стековую память для VLA с областью действия цикла.


иметь максимальный размер, указанный во время компиляции, использовать его для выделения буфера фиксированного размера … расточительно

Для особого случая, как вы упомянули, вы можете иметь буфер фиксированного размера как часть объекта (размер как параметр шаблона), и используйте его, если он достаточно большой. Если нет, то динамически распределяйте. Может быть, использовать элемент указателя, чтобы указать на внутренний или внешний буфер, и флаг, чтобы запомнить, нужно ли delete это или нет в деструкторе. (Вам нужно избегать delete в массиве, который является частью объекта, конечно.)

// optionally static_assert (! (internalsize & (internalsize-1), "internalsize not a power of 2")
// if you do anything that's easier with a power of 2 size
template <type T, size_t internalsize>
class my_container {
T *data;
T internaldata[internalsize];
unsigned used_size;
int allocated_size;   // intended for small containers: use int instead of size_t
// bool needs_delete;     // negative allocated size means internal
}

allocated_size Проверять нужно только когда он растет, поэтому я сделал его подписанным int, чтобы мы могли перегрузить его вместо того, чтобы нуждаться в дополнительном логическом члене.

Обычно контейнер использует 3 указателя вместо указателя + 2 целых числа, но если вы не будете часто увеличивать / уменьшать, тогда мы экономим место (на x86-64, где int это 32 бита и указатели являются 64-битными), и разрешить эту перегрузку.

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

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

3

Другие решения

Других решений пока нет …