Выше, чем ожидалось, использование памяти с VirtualAlloc; в чем дело?

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


На первый взгляд, следующий 64-битный код выделяет (и осуществляет доступ) мегабайты по 4 Мб с использованием VirtualAlloc (всего 4 Гбайт):

const size_t N=4;  // Tests with this many Gigabytes
const size_t pagesize4k=4096;
const size_t npages=(N<<30)/pagesize4k;

BOOST_AUTO_TEST_CASE(test_VirtualAlloc) {

  std::vector<void*> pages(npages,0);
  for (size_t i=0;i<pages.size();++i) {
    pages[i]=VirtualAlloc(0,pagesize4k,MEM_RESERVE|MEM_COMMIT,PAGE_READWRITE);
    *reinterpret_cast<char*>(pages[i])=1;
  }

  // Check all allocs succeeded
  BOOST_CHECK(std::find(pages.begin(),pages.end(),nullptr)==pages.end()); 

  // Free what we allocated
  bool trouble=false;
  for (size_t i=0;i<pages.size();++i) {
    const BOOL err=VirtualFree(pages[i],0,MEM_RELEASE);
    if (err==0) trouble=true;
  }
  BOOST_CHECK(!trouble);
}

Однако при его выполнении "Рабочий набор" увеличивается в диспетчере задач Windows (и подтверждается значением "прилипания" в столбце "Пиковый рабочий набор") с базовой линии ~200 000 КБ (~200 МБайт) до более 6 000 000 или 7 000 000 КБ. (протестировано на 64-битной Windows7, а также на 64-битной Server 2003 и Server 2008 с виртуализацией ESX; к сожалению, я не обратил внимание на то, на каких системах наблюдалось различное число).

Еще один очень похожий тестовый пример в том же исполняемом модульном тесте - тесты one-mega 4k malloc (за которыми следуют освобождения), который расширяется только на ожидаемые 4 Гбайт при работе.

Я не понимаю: VirtualAlloc имеет довольно высокие накладные расходы на выделение ресурсов? Это явно значительная часть размера страницы, если так; зачем так много лишнего и для чего это нужно? Или я неправильно понимаю, что на самом деле означает "рабочий набор"? Что тут происходит?

Обновление: со ссылкой на ответ Ганса, я отмечаю, что это не удается с нарушением прав доступа при доступе ко второй странице, поэтому все, что происходит, не так просто, как выделение, округленное до "гранулярности" 64 КБ.

char*const ptr = reinterpret_cast<char*>(
  VirtualAlloc(0, 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)
);
ptr[0] = 1;
ptr[4096] = 1;

Обновление: теперь на экземпляре AWS/EC2 Windows2008 R2, с установленным VisualStudioExpress2013, я не могу воспроизвести проблему с этим минимальным кодом (скомпилированным 64-битным), который завершается рабочим набором пиков без лишних накладных расходов, равным 4335816 КБ, что составляет номер, который я ожидал увидеть изначально. Так что либо в других машинах, на которых я работаю, есть что-то иное, либо в исполняемом тесте, основанном на буст-тесте, который использовался в предыдущем тестировании. Биззаро, продолжение следует...

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>

#include <vector>

int main(int, char**) {

    const size_t N = 4;
    const size_t pagesize4k = 4096;
    const size_t npages = (N << 30) / pagesize4k;

    std::vector<void*> pages(npages, 0);
    for (size_t i = 0; i < pages.size(); ++i) {
        pages[i] = VirtualAlloc(0, pagesize4k, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
        *reinterpret_cast<char*>(pages[i]) = 1;
    }

    Sleep(5000);

    for (size_t i = 0; i < pages.size(); ++i) {
        VirtualFree(pages[i], 0, MEM_RELEASE);
    }

    return 0;
}

Окончательное обновление: извинения! Я бы удалил этот вопрос, если бы мог, потому что оказалось, что наблюдаемые проблемы были полностью связаны с непосредственным предшествующим юнит-тестом в наборе тестов, который использовал "масштабируемый распределитель" TBB для выделения / освобождения пары гигабайт вещей. Кажется, масштабируемый распределитель фактически сохраняет такие выделения в своем собственном пуле, а не возвращает их в систему (см., Например, здесь или здесь). Стало очевидным, как только я провел тесты индивидуально с достаточным количеством Sleep после того, как они наблюдают за своим рабочим набором по завершении в диспетчере задач (может ли быть что-нибудь предпринято с поведением TBB, может быть интересным вопросом, но как-то вопрос здесь - красная сельдь).

2 ответа

Решение

Оказывается, что наблюдаемые проблемы были полностью связаны с непосредственным предшествующим юнит-тестом в наборе тестов, который использовал "масштабируемый распределитель" TBB для выделения / освобождения пары гигабайт содержимого. Кажется, масштабируемый распределитель фактически сохраняет такие выделения в своем собственном пуле, а не возвращает их в систему (см., Например, здесь или здесь). Стало очевидным, как только я провел тесты индивидуально с достаточным количеством Sleep после них наблюдать за выполнением рабочего набора в диспетчере задач.

   pages[i]=VirtualAlloc(0,pagesize4k,MEM_RESERVE|MEM_COMMIT,PAGE_READWRITE);

Вы не получите 4096 байт, оно будет округлено до минимально допустимого распределения. Это SYSTEM_INFO.dwAllocationGranularity, это было 64 КБ в течение длительного времени. Это очень простая мера фрагментации адресного пространства.

Таким образом, вы выделяете гораздо больше, чем вы думаете.

Другие вопросы по тегам