Стратегия восстановления из NULL == malloc() из-за исчерпания памяти

Чтение блог Мартина Sustrick о проблемах, связанных с предотвращением "поведения undefined" на С++, vs C, в частности, проблемного с malloc() из-за нехватки памяти, мне напомнили о многих, много раз я был расстроен, чтобы знать, что делать в таких случаях.

В виртуальных системах такие условия редки, но на встроенных платформах или когда ухудшение производительности, связанное с ударом виртуальной системы, приравнивается к отказу, как и случай Мартина с ZeroMQ, я решил найти подходящее решение и сделал.

Я хотел спросить читателей StackOverflow, если они пробовали этот подход, и каков был их опыт.

Решение состоит в том, чтобы выделить кусок резервной памяти из кучи с вызовом malloc() в начале программы, а затем использовать этот пул резервной памяти, чтобы предотвратить исчерпание памяти, когда и когда это произойдет. Идея состоит в том, чтобы предотвратить капитуляцию в пользу упорядоченного отступления (я вчера читал отчеты Kesselring defense of Italy), где сообщения об ошибках и IP-адрес сокеты и такие будут работать достаточно долго, чтобы (надеюсь), по крайней мере, рассказать пользователю, что произошло.

#define SPARE_MEM_SIZE (1<<20)  // reserve a megabyte
static void *gSpareMem;

// ------------------------------------------------------------------------------------------------
void *tenacious_malloc(int requested_allocation_size)   {
    static int remaining_spare_size = 0;    // SPARE_MEM_SIZE;
    char err_msg[512];
    void *rtn = NULL;

    // attempt to re-establish the full size of spare memory, if it needs it
    if (SPARE_MEM_SIZE != remaining_spare_size) {
        if(NULL != (gSpareMem = realloc(gSpareMem, SPARE_MEM_SIZE))) {
            remaining_spare_size = SPARE_MEM_SIZE;
            // "touch" the memory so O/S will allocate physical memory
            meset(gSpareMem, 0, SPARE_MEM_SIZE);
            printf("\nSize of spare memory pool restored successfully in %s:%s at line %i :)\n",
                            __FILE__, __FUNCTION__, __LINE__);
        }   else   {
            printf("\nUnable to restore size of spare memory buffer.\n");
        }
    }
    // attempt a plain, old vanilla malloc() and test for failure
    if(NULL != (rtn = malloc(requested_allocation_size))) {
        return rtn;
    }   else  {
        sprintf(err_msg, "\nInitial call to malloc() failed in %s:%s at line %i",
                                                __FILE__, __FUNCTION__, __LINE__);
        if(remaining_spare_size < requested_allocation_size)    {
            // not enough spare storage to satisfy the request, so no point in trying
            printf("%s\nRequested allocaton larger than remaining pool. :(\n\t --- ABORTING --- \n", err_msg);
            return NULL;
        }   else   {
            // take the needed storage from spare memory
            printf("%s\nRetrying memory allocation....\n", err_msg);
            remaining_spare_size -= requested_allocation_size;
            if(NULL != (gSpareMem = realloc(gSpareMem, remaining_spare_size))) {
                // return malloc(requested_allocation_size);
                if(NULL != (rtn = malloc(requested_allocation_size))) {
                    printf("Allocation from spare pool succeeded in %s:%s at line %i :)\n",
                                            __FILE__, __FUNCTION__, __LINE__);
                    return rtn;
                }   else  {
                    remaining_spare_size += requested_allocation_size;
                    sprintf(err_msg, "\nRetry of malloc() after realloc() of spare memory pool "
                        "failed in %s:%s at line %i  :(\n", __FILE__, __FUNCTION__, __LINE__);
                    return NULL;
                }
            }   else   {
                printf("\nRetry failed.\nUnable to allocate requested memory from spare pool. :(\n");
                return NULL;
            }
        }
    }   
}
// ------------------------------------------------------------------------------------------------
int _tmain(int argc, _TCHAR* argv[])    {
    int     *IntVec = NULL;
    double  *DblVec = NULL;
    char    *pString = NULL;
    char    String[] = "Every good boy does fine!";

    IntVec = (int *) tenacious_malloc(100 * sizeof(int));
    DblVec = (double *) tenacious_malloc(100 * sizeof(double));
    pString = (char *)tenacious_malloc(100 * sizeof(String));

    strcpy(pString, String);
    printf("\n%s", pString);


    printf("\nHit Enter to end program.");
    getchar();
    return 0;
}

Ответ 1

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

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

Что касается вашего решения о предварительном распределении некоторого резервного хранилища для использования после того, как malloc завершается сбой, в основном это две версии:

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

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

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

Ответ 2

На современном 64-битном компьютере вы можете значительно увеличить объем памяти, чем у вас есть оперативная память. На практике malloc не подводит. То, что происходит на практике, это то, что ваше приложение начинает биться, и как только вы скажете, что 4 ГБ ОЗУ и ваши распределения превышают это, ваша производительность упадет до нуля, потому что вы меняете, как сумасшедший. Ваша производительность снижается настолько, что вы никогда не дойдете до точки, где malloc не может вернуть память.