Realloc () неправильно освобождает память в Windows

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

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

Вот небольшое консольное приложение, демонстрирующее проблему:

int _tmain(int argc, _TCHAR* argv[])
{
    static const DWORD dwAllocSize = (50 * 1024 * 1024);
    static const DWORD dwReallocSize = 10240;
    static const DWORD dwMaxIterations = 200;

    BYTE* arpMemory[dwMaxIterations];
    memset( arpMemory, 0, sizeof(arpMemory) );

    for( DWORD i = 0; i < dwMaxIterations; i++ )
    {
        arpMemory[i] = (BYTE*) malloc( dwAllocSize );
        if( !arpMemory[i] )
        {
            printf("OUT OF MEMORY after %d iterations!\n", i);
            return -1;
        }

        BYTE* pRealloc = (BYTE*) realloc( arpMemory[i], dwReallocSize );
        if( !pRealloc )
        {
            printf("Realloc FAILED after %d iterations!\n", i);
            return -1;
        }
        else if( pRealloc != arpMemory[i] )
        {
            printf("Warning: Pointer changed: 0x%08X -> 0x%08X\n", arpMemory[i], pRealloc);
            arpMemory[i] = pRealloc;
        }
    }

    printf("Success!\n");

    for( int i = 0; i < dwMaxIterations; i++ )
        free( arpMemory[i] );

    return 0;
}

Приложение постоянно выделяет 50 МБ памяти, а затем сразу же изменяет ее размер до 10 КБ. Если вы запустите его, то обнаружите, что он завершается с ошибкой НЕТ ПАМЯТИ после всего лишь 38 итераций. Это соответствует 2 ГБ изначально выделенной памяти - это предел адресного пространства для приложений Windows.

Интересно, что если вы посмотрите в диспетчере задач, вы увидите, что приложение почти не занимает памяти. Однако malloc () не работает. Это то, что заставляет меня думать, что виртуальное адресное пространство исчерпывается.

(Еще один эксперимент, который нужно попробовать, - закомментировать перераспределение, чтобы память не освобождалась и не перераспределялась. Приложение терпит неудачу точно в том же месте: после 38 итераций. Единственная разница в том, что на этот раз Диспетчер задач отражает все используемые 2 ГБ.)

И последнее: это же приложение работает под Linux. Таким образом, эта проблема realloc () относится только к Windows.

Есть предположения?


person asheffie    schedule 06.02.2012    source источник
comment
Мой вопрос? Я думаю, это известная ошибка в Windows? И, очевидно, есть ли какой-нибудь способ решения этой проблемы, не связанный с копированием памяти?   -  person asheffie    schedule 06.02.2012
comment
В любом случае, у вас, вероятно, есть фундаментальная проблема проектирования, особенно для процессов, которые будут использовать 32-битное адресное пространство. Вы говорите, что я выделяю большой блок памяти, а затем использую realloc (), чтобы сжать его позже, когда я узнаю правильный размер. Это просто напрашивается на неприятности. Очень требовательны очень большие непрерывные блоки адресного пространства. Намного лучше выделить небольшие фрагменты памяти, а затем собрать их вместе.   -  person David Heffernan    schedule 06.02.2012
comment
Хорошая мысль, вы правы. Я использую Visual Studio 2008. (Я немного погуглил, и, похоже, никто не упоминал об этом как о проблеме с этим компилятором - может быть, я первый?)   -  person asheffie    schedule 06.02.2012
comment
fwiw, я взял код и поместил его в проект консольного приложения Studio .NET 2010 C ++ с конфигурацией сборки, установленной на Win32, и увидел тот же результат. Итак, compiler = VC ++, который поставляется со Stuio .NET 2010 для меня. Пришлось попробовать, потому что я ничего не делал с C ++ за 15 лет. :)   -  person joebalt    schedule 06.02.2012
comment
Я не эксперт по низкоуровневым действиям по распределению памяти, но предполагаю, что вы, возможно, фрагментируете кучу. После 38 итераций остается много места в куче, просто нет смежных блоков размером 50 МБ.   -  person Arnold Spence    schedule 06.02.2012
comment
@ArnoldSpence, наверное, прав. Я не ожидал, что реализации malloc будут устойчивы к такому злоупотреблению. Почему разработчики malloc пытались поддерживать такое использование? Ожидается, что вы не будете атаковать эту кучу. Таким образом, вы почти наверняка сможете увидеть тот же эффект на компиляторе C для Linux или Mac с 32-разрядной версией, хотя вам может потребоваться перейти на 4 ГБ, а не на 2 ГБ. Суть в том, что вам нужно будет переработать свой код, чтобы избежать подобной фрагментации виртуального адресного пространства.   -  person David Heffernan    schedule 06.02.2012
comment
Возможно, было бы полезно немного рассказать о контексте. Мой код является частью крупномасштабного приложения, занимающегося обработкой видео. Мы имеем дело с кадром данных за раз, который может быть очень маленьким (сжатие с низкой скоростью передачи данных) или очень большим (несжатое разрешение 4K). В той точке моего кода, где я должен выделить память, я не знаю размер кадра. Поэтому я должен выделить худший вариант развития событий. Позже, когда будут получены все данные кадра, я хочу уменьшить область памяти до фактического размера кадра, который обычно намного меньше первоначально выделенного размера. ...   -  person asheffie    schedule 06.02.2012
comment
... Этот кадр затем передается другим модулям для обработки. Может быть много кадров, и может пройти некоторое время, прежде чем память будет освобождена. Проблема в том, что мы наблюдаем ситуации, когда нам не хватает памяти при обработке кадров небольшого размера. (Эти ошибки нехватки памяти были бы приемлемы для кадров 4K, но не для небольших кадров.) Поскольку скорость важна, мы хотим избежать ненужного копирования памяти. Имея это в виду, видите ли вы какое-нибудь элегантное решение? Или моему модулю потребуется подсказка о размере кадра перед выделением буфера памяти, чего я надеялся избежать?   -  person asheffie    schedule 06.02.2012
comment
Мне кажется, что вам нужно выделить небольшие блоки по запросу и собрать их вместе, или подождать, пока вы не узнаете, насколько велик кадр, прежде чем выполнять распределение. Или переключитесь на 64-битный процесс, и тогда вы сможете в значительной степени злоупотреблять кучей, сколько душе угодно.   -  person David Heffernan    schedule 06.02.2012
comment
Спасибо за отличные ответы! @ArnoldSpence прав: это действительно случай фрагментации памяти. Я подтвердил это, изменив свой пример выше, чтобы выделить серию буферов размером 49 МБ после достижения условия НЕТ ПАМЯТИ. Конечно, все это удалось (до 2 ГБ).   -  person asheffie    schedule 06.02.2012
comment
(К сведению всех, кто читает эту ветку: когда я говорю кадры 4K, я имею в виду кадры с разрешением 4K, а не 4 килобайта.)   -  person asheffie    schedule 06.02.2012
comment
@ Дэвид Хеффернан: Выделение небольших блоков и их соединение по кусочкам не сработает, поскольку в конечном итоге нам нужно, чтобы фреймовая память была непрерывной. Итак, на данный момент я вижу три решения: 1) Сделайте единовременное выделение максимального размера кадра для входящих данных видеокадра и перенесите снижение производительности из-за копирования памяти в меньший буфер, когда мы будем готовы передать данные. 2) Придумайте разумный метод подсказки кадра, чтобы мы не чрезмерно выделяли невероятный объем памяти. 3) Как предложил Дэвид Хеффернан, посмотрим, сможем ли мы переместить весь наш проект для работы в 64-битном адресном пространстве.   -  person asheffie    schedule 06.02.2012
comment
Как приведенный выше код работает в Linux? (Предположительно, среда выполнения gcc и glibc) Единственное, что у меня есть, это то, что он работает с 64-битным Linux и получает 64-битную компиляцию. (Но его Visual Studio по умолчанию использует 32-битные EXE).   -  person selbie    schedule 07.02.2012
comment
@selbie: Я не могу его протестировать, но я не понимаю, почему нельзя ожидать, что этот код будет работать с любой реализацией, которая явно не помещает большие запросы в отдельные диапазоны адресов. Вы бы получили фрагментацию, если бы между моментом выполнения большого выделения и его сжатием были другие распределения, но здесь это не так (по крайней мере, не в примере кода).   -  person Harry Johnston    schedule 07.02.2012
comment
@selbie Я предполагаю, что диспетчер кучи в Linux просто более разумно реализован. Обратите внимание на мой пример, как я выделяю 50 МБ, но затем сразу уменьшаю их до 10 КБ, без каких-либо других выделений между этими двумя операциями. Интеллектуальный диспетчер кучи должен разместить следующее выделение после этого 10-килобайтного блока. Но что происходит под Windows, так это то, что новое выделение помещается в конец первоначального выделения 50 МБ. Возможно также, что это происходит из-за того, что во время выполнения C между моими собственными выделениями были другие выделения, вызывающие фрагментацию.   -  person asheffie    schedule 07.02.2012


Ответы (2)


Делая это, вы фрагментируете кучу. Все, что вы выпускаете с помощью realloc (), добавляется в список свободных блоков. Никогда не будет использоваться снова, потому что вы всегда запрашиваете новый блок большего размера. Эти свободные блоки будут накапливаться, занимая виртуальную память до тех пор, пока их больше не останется. Происходит довольно быстро, если за один раз выбросить почти 50 мегабайт.

Вам нужно будет переосмыслить свой подход.

person Hans Passant    schedule 06.02.2012
comment
Спасибо за отличную помощь! Как я упоминал в ветке комментариев выше, на данный момент я вижу три решения: 1) Сделайте единовременное выделение максимального размера для входящих данных и пострадайте от снижения производительности копии памяти в меньшем буфере (как только мы узнаем фактический размер). size), когда мы будем готовы передать данные. 2) Придумайте метод подсказки разумного размера буфера, чтобы мы не выделяли слишком много памяти. 3) Посмотрим, сможем ли мы переместить весь наш проект для работы в 64-битном адресном пространстве. - person asheffie; 07.02.2012
comment
Просто сделайте наоборот. Начните с малого, используйте realloc (), когда вам нужно больше. Стандартный подход - удвоение распределения. - person Hans Passant; 07.02.2012
comment
Ганс, я не думаю, что эта проблема связана с фрагментацией. Я пытался уменьшить размер каждого последующего запроса, чтобы он умещался в пространстве, освобожденном от предыдущего, и это не имело никакого значения. - person Harry Johnston; 07.02.2012
comment
@ Гарри - Ну, на сколько? С каждым блоком связано множество накладных расходов, вам придется это учитывать. - person Hans Passant; 07.02.2012
comment
@Hans: мой текущий код уменьшает размер выделения на 1 МБ за итерацию. Я также заметил, что при обходе кучи отдельно выделенные блоки адресов не включают диапазоны памяти, помеченные как свободные (PROCESS_HEAP_UNCOMMITTED_RANGE), как это делает основной блок адреса. - person Harry Johnston; 07.02.2012
comment
@ Гарри: Я удивлен, что ты не получил такого же результата, как я, когда я сделал что-то подобное. Я изменил свой пример, чтобы выделить серию буферов размером 49 МБ сразу после достижения условия НЕТ ПАМЯТИ. Все это удалось (до 2 ГБ), потому что все они хорошо вписываются в дыры текущей фрагментированной памяти. - person asheffie; 07.02.2012
comment
@asheffie, да, это странно. Это может зависеть от версии и / или архитектуры Windows. Я опубликовал свой тестовый код и результаты для сравнения, если вам интересно. В любом случае результат один и тот же: независимо от того, можно ли повторно использовать свободное пространство в резервировании виртуальных адресов, сами резервирования остаются того же размера, что приводит к искусственному фрагментированию адресного пространства, потому что выделение памяти никогда не перекрывает несколько резервирований адресов. Итог: вы не можете этого сделать. :-) - person Harry Johnston; 07.02.2012

После некоторых экспериментов и чтения между строк документации я пришел к выводу, что большие выделения памяти (чуть менее 512 КБ для 32-разрядной версии, чуть менее 1 МБ для 64-разрядной версии) используют адресное пространство, выделенное с использованием VirtualAlloc и зарезервировано для этого блок памяти.

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

Дополнительно: вот тестовый код, показывающий, что я вижу:

#include <windows.h>

#include <stdio.h>

DWORD reallocSize = 0x01000;    // 4K

void walkheap(HANDLE heap)
{
    PROCESS_HEAP_ENTRY phe;
    MEMORY_BASIC_INFORMATION mbi;

    phe.lpData = NULL;

    for (;;)
    {
        if (!HeapWalk(heap, &phe))
        {
            printf("HeapWalk: %u\n", GetLastError());
            return;
        }
        printf("%08x %08x %08x %08x %08x ", phe.lpData, phe.cbData, phe.cbOverhead, phe.iRegionIndex, phe.wFlags);
        if (VirtualQuery(phe.lpData, &mbi, sizeof(mbi)) != 0)
        {
            printf("--> %08x\n",mbi.AllocationBase);
        }
        else
        {
            printf("--> (error %u)\n", GetLastError());
        }
    }
}

void alloc(HANDLE heap, DWORD count, DWORD size)
{
    BYTE* ptr;
    BYTE* pRealloc;

    ptr = (BYTE *)HeapAlloc(heap, 0, size);
    printf("Pointer %u is %08x (%08x)\n", count, ptr, size);

    pRealloc = (BYTE*) HeapReAlloc( heap, 0, ptr, reallocSize);
    if( pRealloc != ptr)
    {
        printf("Pointer %u changed to %08x\n", count, pRealloc);
    }
}

int main(int argc, char ** argv)
{
    HANDLE heap;

    heap = HeapCreate(0, 0, 0);
    if (heap == NULL)
    {
        printf("HeapCreate: %u\n", GetLastError());
        return 1;
    }

    walkheap(heap);

    alloc(heap, 1, 0x08000);
    alloc(heap, 2, 0x08000);
    alloc(heap, 3, 0x08000);
    alloc(heap, 4, 0x08000);

    alloc(heap, 10, 0x20000000);
    alloc(heap, 11, 0x20000000);
    alloc(heap, 12, 0x20000000);
    alloc(heap, 13, 0x20000000);

    alloc(heap, 20, 0x10000000);
    alloc(heap, 21, 0x10000000);
    alloc(heap, 22, 0x10000000);
    alloc(heap, 23, 0x10000000);

    walkheap(heap);

    return 0;
}

и мои результаты (см. PROCESS_HEAP_ENTRY структура):

Address  Alloc    Overhead Region   Flags        Virtual Address Range Base

00420000 00000588 00000000 00000000 00000001 --> 00420000
004207e8 000007f8 00000010 00000000 00000000 --> 00420000
00421000 0003f000 00000000 00000000 00000002 --> 00420000
HeapWalk: 259
Pointer 1 is 004207e0 (00008000)
Pointer 2 is 004217f8 (00008000)
Pointer 3 is 00422810 (00008000)
Pointer 4 is 00423828 (00008000)
Pointer 10 is 00740020 (20000000)
Pointer 11 is 20750020 (20000000)
Pointer 12 is 52580020 (20000000)
Pointer 13 is 00000000 (20000000)
Pointer 20 is 40760020 (10000000)
Pointer 21 is 00000000 (10000000)
Pointer 22 is 00000000 (10000000)
Pointer 23 is 00000000 (10000000)
00420000 00000588 00000000 00000000 00000001 --> 00420000
004207e0 00001000 00000018 00000000 00000004 --> 00420000
004217f8 00001000 00000018 00000000 00000004 --> 00420000
00422810 00001000 00000018 00000000 00000004 --> 00420000
00423828 00001000 00000018 00000000 00000004 --> 00420000
00424848 0000e798 00000010 00000000 00000000 --> 00420000
00433000 0002d000 00000000 00000000 00000002 --> 00420000
00740020 00001000 00000020 00000040 00000024 --> 00740000
20750020 00001000 00000020 00000040 00000024 --> 20750000
52580020 00001000 00000020 00000040 00000024 --> 52580000
40760020 00001000 00000020 00000040 00000024 --> 40760000
HeapWalk: 259

Можно видеть, что небольшие выделения плотно упакованы, но все большие выделения находятся в отдельных выделениях виртуальных адресов. Свободное место в отдельных выделениях не используется. Кроме того, только выделение основного виртуального адреса имеет любые блоки кучи, которые помечены как свободное пространство (флаг равен 0 или 2).

person Harry Johnston    schedule 06.02.2012