Выделяет ли память, а затем освобождение, побочный эффект в программе на С++?

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

delete[] new char[10];

Он ничего не полезен. Но имеет ли он побочный эффект? Является ли распределение кучи сразу же после освобождения, которое считается побочным эффектом?

Ответ 1

Это до реализации. Выделение и освобождение памяти не является "наблюдаемым поведением", если только реализация не решит, что это наблюдаемое поведение.

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

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

Я уверен, что вам не разрешено полагаться на исключение из new[], чтобы "доказать", закончилось ли у вас нехватка памяти. Другими словами, только потому, что new char[10] не выбрасывает это время, не означает, что он не будет бросать после того, как вы освободите память и повторите попытку. И только потому, что он бросил последний раз, и с тех пор вы ничего не освободили, это не значит, что он бросит это время. Поэтому я не вижу причин по этим причинам, почему два вызова не могут быть устранены - нет ситуации, когда стандарт гарантирует, что new char[10] будет бросать, поэтому нет необходимости в реализации, чтобы выяснить, будет ли это или нет, Насколько вам известно, какой-то другой процесс в системе освободил 10 байт непосредственно перед вызовом new[] и выделил его сразу после вызова delete[].

[*]

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

// new[]
global_last_allocation = global_next_allocation;
global_next_allocation += 10 + sizeof(size_t);
char *tmp = global_last_allocation;
*((size_t *)tmp) = 10; // code to handle alignment requirements is omitted
tmp += sizeof(size_t);

// delete[]
tmp -= sizeof(size_t);
if (tmp == global_last_allocation) {
    global_next_allocation -= 10 + *((size_t*)tmp);
}

Что можно было бы почти удалить, если ничего не изменено, оставив global_last_allocation = global_next_allocation;. Вы также можете избавиться от этого, сохранив предыдущее значение last в заголовке блока вместе с размером и восстановив это предыдущее значение при освобождении последнего выделения. Тем не менее, для реализации довольно реалистичной реализации распределителя памяти вам понадобится однопоточная программа с программистом скорости, который уверен, что программа не отбирает больше памяти, чем была доступна для начала.

Ответ 2

new[] и delete[] могут в конечном итоге привести к системным вызовам. Кроме того, new[] может выбраться. Имея это в виду, я не вижу, как последовательность new-delete может быть законно рассмотрена без побочных эффектов и оптимизирована.

(Здесь я предполагаю, что не задействована перегрузка new[] и delete[].)

Ответ 3

Нет. Он также не должен быть удален компилятором и не рассматривается как побочный эффект. Рассмотрим ниже:

struct A {
  static int counter;
  A () { counter ++; }
};

int main ()
{
  A obj[2]; // counter = 2
  delete [] new A[3]; // counter = 2 + 3 = 5
}

Теперь, если компилятор удаляет это как побочный эффект, логика идет не так. Итак, даже если вы ничего не делаете, компилятор всегда будет считать, что что-то полезное происходит (в конструкторе). Вот почему:

A(); // simple object allocation and deallocation

не оптимизирован.

Ответ 4

Компилятор не может видеть реализацию delete [] и new [] и должен предположить, что он делает.

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

Ответ 5

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

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

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