Почему realloc съедает тонны памяти?

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

У меня есть приложение с циклом, который может выполняться миллионы раз. Вместо нескольких тысяч или миллионов вызовов malloc/free в этом цикле я хотел бы сделать один malloc заранее, а затем от нескольких тысяч до миллионов вызовов realloc.

Но я столкнулся с проблемой, когда мое приложение потребляет несколько ГБ памяти и убивает себя, когда я использую realloc. Если я использую malloc, мое использование памяти в порядке.

Если я запускаю меньшие наборы тестовых данных с memtest valgrind, он не сообщает об утечках памяти ни с malloc, ни с realloc.

Я проверил, что сопоставляю каждый объект с malloc-ed (а затем realloc-ed) с соответствующим free.

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

Сначала у меня было что-то вроде этого, которое использует malloc и работает правильно:

Код Malloc

void A () {
    do {
        B();
    } while (someConditionThatIsTrueForMillionInstances);
}

void B () {
    char *firstString = NULL;
    char *secondString = NULL;
    char *someOtherString;

    /* populate someOtherString with data from stream, for example */

    C((const char *)someOtherString, &firstString, &secondString);

    fprintf(stderr, "first: [%s] | second: [%s]\n", firstString, secondString);

    if (firstString)
        free(firstString);
    if (secondString)
        free(secondString);
}

void C (const char *someOtherString, char **firstString, char **secondString) {
    char firstBuffer[BUFLENGTH];
    char secondBuffer[BUFLENGTH];

    /* populate buffers with some data from tokenizing someOtherString in a special way */

    *firstString = malloc(strlen(firstBuffer)+1);
    strncpy(*firstString, firstBuffer, strlen(firstBuffer)+1);

    *secondString = malloc(strlen(secondBuffer)+1);
    strncpy(*secondString, secondBuffer, strlen(secondBuffer)+1);
}

Это прекрасно работает. Но я хочу что-то быстрее.

Теперь я тестирую realloc аранжировку, которая malloc только один раз:

Код Realloc

void A () {
    char *firstString = NULL;
    char *secondString = NULL;

    do {
        B(&firstString, &secondString);
    } while (someConditionThatIsTrueForMillionInstances);

    if (firstString)
        free(firstString);
    if (secondString)
        free(secondString);
}

void B (char **firstString, char **secondString) {
    char *someOtherString;

    /* populate someOtherString with data from stream, for example */

    C((const char *)someOtherString, &(*firstString), &(*secondString));

    fprintf(stderr, "first: [%s] | second: [%s]\n", *firstString, *secondString);
}

void C (const char *someOtherString, char **firstString, char **secondString) {
    char firstBuffer[BUFLENGTH];
    char secondBuffer[BUFLENGTH];

    /* populate buffers with some data from tokenizing someOtherString in a special way */

    /* realloc should act as malloc on first pass through */

    *firstString = realloc(*firstString, strlen(firstBuffer)+1);
    strncpy(*firstString, firstBuffer, strlen(firstBuffer)+1);

    *secondString = realloc(*secondString, strlen(secondBuffer)+1);
    strncpy(*secondString, secondBuffer, strlen(secondBuffer)+1);
}

Если я посмотрю на вывод free -m в командной строке, когда я запускаю этот тест на основе realloc с большим набором данных, который вызывает условие миллиона циклов, моя память уменьшится с 4 ГБ до 0, и приложение вылетит.

Что мне не хватает в использовании realloc, что вызывает это? Извините, если это глупый вопрос, и заранее спасибо за ваш совет.


person Alex Reynolds    schedule 15.11.2010    source источник
comment
Можете ли вы также показать вывод из strace? Было бы полезно узнать, на что сопоставляются системные вызовы realloc и malloc/free.   -  person Flexo    schedule 16.11.2010
comment
Возможно, вы захотите изучить пользовательскую функцию распределения, которая в основном получает огромный кусок через malloc() и управляет им как пользовательским пространством кучи. Это своего рода старая школа, но если вы знаете, что делаете, это может работать очень хорошо.   -  person    schedule 16.11.2010


Ответы (3)


realloc должен скопировать содержимое из старого буфера в новый буфер, если операция изменения размера не может быть выполнена на месте. Пара malloc/free может быть лучше, чем realloc, если вам не нужно хранить исходную память.

Вот почему realloc может временно требовать больше памяти, чем пара malloc/free. Вы также поощряете фрагментацию, постоянно чередуя reallocs. То есть вы в основном делаете:

malloc(A);
malloc(B);

while (...)
{
    malloc(A_temp);
    free(A);
    A= A_temp;
    malloc(B_temp);
    free(B);
    B= B_temp;
}

В то время как исходный код делает:

while (...)
{
    malloc(A);
    malloc(B);
    free(A);
    free(B);
}

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

person MSN    schedule 15.11.2010
comment
Есть ли способ периодически обновлять фрагментированную кучу, скажем, каждые x число итераций? - person Alex Reynolds; 16.11.2010
comment
@Alex, это зависит от фактического распределителя. Обратите внимание, что моя идея фрагментации не очень точна; это действительно зависит от базовой реализации. Если вы используете Windows, вы можете попробовать использовать HeapCompact в глобальной куче, но на самом деле это ничего не гарантирует. - person MSN; 16.11.2010
comment
Хм, кажется, вся эта фрагментация противоречит тому, что я слышал и читал о том, как рекомендуется использовать realloc, когда есть много дорогостоящих вызовов malloc/free. Возможно, мне следует изучить буфер фиксированного размера в стеке. :( - person Alex Reynolds; 16.11.2010
comment
@Alex: следите за текущими размерами выделения firstString и secondString. Затрудняйтесь realloc их только в том случае, если они должны расти, и квантизируйте их размер (например, сделайте их следующей наибольшей степенью двойки над размером, который вам нужен). - person caf; 16.11.2010
comment
@ Алекс, опять же, идея фрагментации не так точна; это действительно зависит от распределителя. С очень простым распределителем на основе стека (т. е. он освобождает только непрерывную память и заканчивается выделенной областью) вы можете получить патологически плохое поведение распределения со стратегией, основанной на реальном распределении. На другом аллокаторе, настроенном на общие шаблоны, вы не можете. Это действительно зависит от распределителя. Но в большинстве случаев realloc в худшем случае потребует, чтобы был доступен старый размер плюс новый размер. - person MSN; 17.11.2010

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

Управление памятью не просто. Плохие стратегии распределения приводят к фрагментации, ужасной производительности и т. д. Лучшее, что вы можете сделать, это избегать введения каких-либо дополнительных ограничений, чем вам абсолютно необходимо (например, использование realloc, когда это не нужно), освобождайте как можно больше памяти, когда вы закончите с это и распределять большие блоки связанных данных вместе в одном выделении, а не небольшими частями.

person R.. GitHub STOP HELPING ICE    schedule 16.11.2010

Вы ожидаете, что &(*firstString) будет таким же, как firstString, но на самом деле он принимает адрес аргумента вашей функции, а не передает адрес указателей в A. Таким образом, каждый раз, когда вы вызываете, вы делаете копию NULL, перераспределяете новую память, теряете указатель на новую память и повторяете. Вы можете легко убедиться в этом, увидев, что в конце A исходные указатели все еще нулевые.

РЕДАКТИРОВАТЬ: Ну, это потрясающая теория, но я, кажется, ошибаюсь в компиляторах, которые у меня есть для тестирования.

person Ben Jackson    schedule 15.11.2010
comment
Я не уверен, что понимаю. Оператор fprintf в B не показывает NULL для значений двух строк, т. е. токенизация и strncpy выполнены успешно. - person Alex Reynolds; 16.11.2010
comment
Извините, где я сказал B, я имел в виду A (в версии 2); Я слишком далеко прокрутил вопрос, когда получал имя функции. - person Ben Jackson; 16.11.2010
comment
Если я использую небольшие наборы данных, приложение realloc (версия 2) работает, т.е. я не вижу NULL для значений firstString и secondString, так как событие копирования прошло успешно. - person Alex Reynolds; 16.11.2010
comment
&*first_string совпадает с first_string. Я был бы бесконечно удивлен, если бы это было не так. - person MSN; 16.11.2010