realloc, но только первые несколько байтов имеют смысл

Предположим, я использовал ptr = malloc(old_size); для выделения блока памяти размером old_size байт. Только первые header_size байта имеют смысл. Я собираюсь увеличить размер до new_size.

new_size больше old_size, а old_size больше header_size.

до:

/- - - - - - - old_size - - - - - - - \
+===============+---------------------+
 \-header_size-/

после:

/- - - - - - - - - - - - - - - new_size - - - - - - - - - - - - - - - - - - -\
+===============+------------------------------------------------------------+
\- header_size-/

Мне все равно, что хранится после ptr + header_size, потому что я буду читать туда некоторые данные.

способ 1: идите прямо к new_size

ptr = realloc(ptr, new_size);

метод 2: уменьшить до header_size и увеличить до new_size

ptr = realloc(ptr, header_size);
ptr = realloc(ptr, new_size);

способ 3: выделить новый блок памяти и скопировать первые header_size байт

void *newptr = malloc(new_size);
memcpy(newptr, ptr, header_size);
free(ptr);
ptr = newptr;

Что быстрее?


person lqs    schedule 06.11.2012    source источник
comment
@nos Я напишу некоторые данные за пределами header_size после перераспределения, поэтому мне все равно, что там.   -  person lqs    schedule 06.11.2012


Ответы (3)


Почти наверняка это зависит от значений old_size, new_size и header_size, а также от реализации. Вам нужно будет выбрать некоторые значения и измерить.

1), вероятно, лучше всего в случае, когда header_size == old_size-1 && old_size == new_size-1, так как это дает вам наилучшие шансы на то, что один realloc в основном не работает. (2) в этом случае должен быть лишь немного медленнее (2 почти без операций немного медленнее, чем 1).

3), вероятно, лучше всего в случае, когда header_size == 1 && old_size == 1024*1024 && new_size == 2048*1024, потому что realloc пришлось бы переместить выделение, но вы избегаете копирования 1 МБ данных, которые вам не нужны. (2) в этом случае должно быть лишь немного медленнее.

2) лучше всего подходит, когда header_size намного меньше, чем old_size, а new_size находится в диапазоне, при котором realloc с достаточной вероятностью переместится, но также достаточно вероятно, что этого не произойдет. Тогда вы не можете предсказать, какой из (1) и (3) будет немного быстрее, чем (2).

При анализе (2) я предположил, что функция realloc вниз приблизительно свободна и возвращает тот же указатель. Это не гарантируется. Я могу думать о двух вещах, которые могут испортить вам жизнь:

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

Любой из них может сделать (2) значительно дороже, чем (1). Таким образом, это деталь реализации, является ли (2) хорошим способом хеджирования ваших ставок между преимуществами (1) (иногда избегая копирования чего-либо) и преимуществами (3) (иногда избегая слишком большого копирования).

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

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

person Steve Jessop    schedule 06.11.2012

Ни malloc (для всего блока), ни realloc (для пространства за пределами размера старого блока при увеличении размера) не гарантируют, что будет содержать полученная вами память, поэтому, если вы хотите, чтобы эти лишние байты были равны нулю (например), вам придется сделать это самостоятельно с чем-то вроде:

// ptr contains current block.
void *saveptr = ptr;
ptr = realloc (ptr, new_size);
if (ptr == NULL) {
    // do something intelligent like recover saveptr and exit.
}
memset (ptr + header_size, 0, new_size - header_size);

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

Вызов его дважды для сжатия и расширения или вызов malloc-new/memcpy/free-old вряд ли будет столь же эффективным, хотя, как и при любой оптимизации, вы должны измерять, а не гадать!

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

+-----------+   ^        +-----------+ <- At same address,
| Old block |   | Need   | New block |      no copying
|           |   | this   |           |      involved.
+-----------+   | much   |           |
| Free      |   | now.   |           |
|           |   v        +-----------+
|           |            | Free      |
|           |            |           |
+-----------+            +-----------+
person paxdiablo    schedule 06.11.2012
comment
Хм? Конечно, realloc() гарантирует, что новая память будет содержать столько, сколько поместится из старого блока. - person unwind; 06.11.2012
comment
@unwind: я имел в виду за пределами старой границы, а не память в старом блоке. Я уточню. - person paxdiablo; 06.11.2012
comment
@lqs, поскольку вы заявили в комментарии, что вам все равно, что содержит не-заголовок, я упростил свой ответ, чтобы он подходил. - person paxdiablo; 06.11.2012

Вероятно, это зависит от размеров и необходимости копирования.

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

Метод 2 скопирует только то, что вам нужно сохранить, так как вы заранее отбрасываете все остальное.

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

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

person glglgl    schedule 06.11.2012
comment
Метод 2 — единственный, при котором оба полностью избегают копирования, если после блока есть свободное место, и в противном случае копирует только минимальный объем памяти. Метод 1 является наиболее эффективным в лучшем случае, но, возможно, и наименее эффективным в худшем случае. В настоящее время это зависит от большого количества данных, о которых мы говорим, стоят ли дополнительные накладные расходы на функции. - person ams; 06.11.2012
comment
@ams Существуют ли на самом деле realloc реализации, которые рассматривают свободные блоки после того, как блок будет realloc обработан? Мне это кажется маловероятным. - person pmr; 06.11.2012
comment
@pmr: даже если они не пытаются объединить блоки, в случае, когда new_size - old_size мало, в конце блока может быть свободное место, которое можно использовать для соответствия новому размеру из-за округления old_size. - person Steve Jessop; 06.11.2012
comment
@pmr Почему бы реализации не расширить выделение на следующее свободное пространство, если оно есть? Конечно, кто-то должен реализовать это таким образом, но есть много умных людей! Однако на самом деле вы даже не можете предположить, что уменьшение размера выделения не приведет к копированию. - person ams; 06.11.2012
comment
@pmr По крайней мере, в некоторых реализациях malloc используется mmap для выделения более одной страницы (обычно 4 КБ), поэтому увеличение размера выделения почти не требуется. Это зависит от того, насколько переполнено адресное пространство, но на 64-битной машине это, вероятно, не проблема. - person ams; 06.11.2012