【问题标题】:Higher than expected memory usage with VirtualAlloc; what's going on?VirtualAlloc 的内存使用率高于预期;这是怎么回事?
【发布时间】:2014-01-21 14:32:16
【问题描述】:

重要提示:在您在这里投入过多时间之前,请向下滚动至“最终更新”。事实证明,主要的教训是要注意单元测试套件中其他测试的副作用,并且总是在得出结论之前孤立地重现事物!


从表面上看,下面的 64 位代码使用 VirtualAlloc(总共 4GByte)分配(并访问)一兆 4k 页面:

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);
}

但是,在执行时,“工作集”reported in Windows Task Manager(并通过“峰值工作集”列中的值“sticking”确认)从基线 ~200,000K (~200MByte) 增加到超过 6,000,000 或 7,000,000 K(在 64 位 Windows7 以及 ESX 虚拟化的 64 位 Server 2003 和 Server 2008 上测试;不幸的是,我没有注意到观察到的各种数字发生在哪些系统上)。

同一个 unittest 可执行文件中的另一个非常相似的测试用例测试了一个 4k 的 malloc(随后是 frees),并且在运行时仅扩展了预期的 4GByte 左右。

我不明白:VirtualAlloc 是否具有相当高的 per-alloc 开销?如果是这样,它显然是页面大小的很大一部分;为什么需要这么多额外的东西,它有什么用?还是我误解了“工作集”报告的实际含义?这是怎么回事?

更新:参考 Hans 的回答,我注意到这在第二页访问中因访问冲突而失败,因此无论发生什么都不像分配被四舍五入那样简单64K“粒度”。

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

更新:现在在安装了 VisualStudioExpress2013 的 AWS/EC2 Windows2008 R2 实例上,我无法用这个最小代码(编译为 64 位)重现问题,这显然会产生开销 - 4,335,816K 的免费峰值工作集,这是我最初期望看到的数字。因此,要么我正在运行的其他机器有所不同,要么之前测试中使用的基于 boost-test 的 exe 有所不同。 比扎罗,待续……

#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 的“可扩展分配器”来分配/解除分配几个 GByte的东西。似乎可扩展分配器实际上将此类分配保留在它自己的池中,而不是将它们返回给系统(参见例如herehere)。一旦我使用足够的Sleep 单独运行测试以观察他们在任务管理器中的完成工作集(是否可以对 TBB 行为做任何事情可能是一个有趣的问题,但问题在这里是红鲱鱼)。

【问题讨论】:

标签: c++ winapi 64-bit virtual-memory virtualalloc


【解决方案1】:
   pages[i]=VirtualAlloc(0,pagesize4k,MEM_RESERVE|MEM_COMMIT,PAGE_READWRITE);

您不会得到 4096 字节,它将被四舍五入到允许的最小分配。也就是SYSTEM_INFO.dwAllocationGranularity,好久都64KB了。这是一个非常基本的地址空间碎片对策。

所以你分配的方式比你想象的要多。

【讨论】:

  • VirtualAlloc 是在 64KB 粒度边界上保留和提交,还是在 64KB 上保留,但在页面 (4KB) 边界和页面大小的块中提交?
  • 嗯,这是个有趣的信息;我的解释更像是 marcin_i 的解释。我有点惊讶,我不只是崩溃和烧毁,或者看到工作集爆炸到 64GByte,如果是这样的话……但也许我只触摸每个 alloc 中的第一页的事实可以拯救我。易于测试...我还应该能够在每个分配上都点击 *(reinterpret_cast(pages[i])+4096)=1 而不会崩溃?不幸的是,直到下周的某个时候,我才能再次访问 Windows 机器......
  • 是的,将 VM 映射到 RAM 仍然是基于页面的,粒度为 4096 字节。只需将提交大小与工作集进行比较即可查看差异。
  • (好吧,我实际上启动了一个 Windows EC2 实例来玩这个)。那么“粒度”地址范围内其他 60K 的页面处于什么状态?他们显然也没有承诺,因为访问 +4096 字节偏移会产生访问冲突。它们是否保留但未提交?
  • 尝试这个(从来没有故意弄错这个),他们看起来只是不可用。无法提交或重新分配它们。
【解决方案2】:

事实证明,观察到的问题完全是由于测试套件中的前一个单元测试使用 TBB 的“可扩展分配器”来分配/取消分配几个 GByte 的东西。看起来可扩展的分配器实际上将这些分配保留在它自己的池中,而不是将它们返回给系统(参见例如herehere)。一旦我单独运行测试并使用足够的Sleep 在他们之后观察他们在任务管理器中的完成工作集,就会变得很明显。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-02-29
    • 2016-05-05
    • 2015-07-05
    • 2014-03-14
    • 2011-07-06
    • 2011-05-04
    • 2010-09-29
    • 2016-02-19
    相关资源
    最近更新 更多