7

我在带有 MSVC 9.0 的 Windows 7 上使用 C++,并且还能够在带有 MSVC 9.0 的 Windows XP SP3 上进行测试和重现。

如果我分配 1 GB 的 0.5 MB 大小的对象,当我删除它们时,一切正常并且按预期运行。但是,如果我在删除它们时分配 1 GB 的 0.25 MB 大小的对象,则内存保持保留状态(地址空间监视器中的黄色),从那时起将只能用于小于 0.25 MB 的分配。

这个简单的代码将让您通过更改哪个结构是 typedef'd 来测试这两种情况。在分配并删除结构后,它将分配 1 GB 的 1 MB 字符缓冲区,以查看字符缓冲区是否会使用结构曾经占用的内存。

struct HalfMegStruct
{
    HalfMegStruct():m_Next(0){}

    /* return the number of objects needed to allocate one gig */
    static int getIterations(){ return 2048; }

    int m_Data[131071];
    HalfMegStruct* m_Next;
};

struct QuarterMegStruct
{
    QuarterMegStruct():m_Next(0){}

    /* return the number of objects needed to allocate one gig */
    static int getIterations(){ return 4096; }

    int m_Data[65535];
    QuarterMegStruct* m_Next;
};

// which struct to use
typedef QuarterMegStruct UseType;

int main()
{
    UseType* first = new UseType;
    UseType* current = first;

    for ( int i = 0; i < UseType::getIterations(); ++i )
        current = current->m_Next = new UseType;

    while ( first->m_Next )
    {
        UseType* temp = first->m_Next;
        delete first;
        first = temp;
    }

    delete first;

    for ( unsigned int i = 0; i < 1024; ++i )
        // one meg buffer, i'm aware this is a leak but its for illustrative purposes. 
        new char[ 1048576 ]; 

    return 0;
}

您可以在下面看到我在Address Space Monitor中的结果。让我强调一下,这两个最终结果之间的唯一区别是分配给 1 GB 标记的结构的大小

四分之一梅格 半梅格

对我来说,这似乎是一个相当严重的问题,许多人可能正在遭受痛苦,甚至不知道。

  • 那么这是设计使然还是应该将其视为错误?
  • 我可以让较小的已删除对象实际上可以免费供较大的分配使用吗?
  • 出于好奇,Mac 或 Linux 机器会遇到同样的问题吗?
4

5 回答 5

9

我不能肯定地说是这种情况,但这看起来确实像内存碎片(以其多种形式之一)。分配器 (malloc) 可能会保留不同大小的存储桶以实现快速分配,在您释放内存后,它不会直接将其返还给操作系统,而是保留存储桶以便以后可以处理相同大小的分配相同的记忆。如果是这种情况,内存将可用于相同大小的进一步分配。

这种类型的优化通常对对象禁用,因为即使不使用它也需要保留内存。如果阈值介于您的两种尺寸之间,则可以解释这种行为。

请注意,虽然您可能认为这很奇怪,但在大多数程序(不是测试,而是现实生活)中,内存使用模式是重复的:如果您要求 100k 块一次,通常情况下您会再次这样做. 保留内存可以提高性能并实际上减少来自同一存储桶授予的所有请求的碎片。

如果您想投入一些时间,您可以通过分析行为来了解分配器的工作原理。编写一些测试,将获取大小 X,释放它,然后获取大小 Y,然后显示内存使用情况。固定 X 的值并与 Y 一起玩。如果两个大小的请求是从同一个存储桶授予的,您将没有保留/未使用的内存(左图),而当大小从不同的存储桶授予时,您会看到对右侧图像的影响。

我通常不为 Windows 编写代码,我什至没有 Windows 7,所以我不能肯定地说是这样,但看起来确实如此。

于 2011-03-24T08:54:29.527 回答
2

我可以在 Windows 7 下使用 g++ 4.4.0 确认相同的行为,因此它不在编译器中。事实上,程序在getIterations()返回3590或更多时失败——你得到相同的截止值吗?这看起来像是 Windows 系统内存分配中的错误。有见识的人讲内存碎片说得很好,但是这里的东西都被删了,所以观察到的行为绝对不应该发生。

于 2011-03-24T09:08:43.837 回答
1

使用您的代码,我执行了您的测试并得到了相同的结果。我怀疑大卫罗德里格斯在这种情况下是对的。

我跑了测试,结果和你一样。似乎可能存在这种“桶”行为。

我也尝试了两种不同的测试。我没有使用 1MB 缓冲区分配 1GB 数据,而是以与删除后首次分配内存相同的方式分配。第二个测试我分配了清理后的半兆缓冲区,然后分配了四分之一兆缓冲区,每个缓冲区加起来为 512MB。两个测试最后都有相同的内存结果,只有 512 被分配了一个没有大块的保留内存。

正如大卫所提到的,大多数应用程序倾向于分配相同的大小。人们可以很清楚地看到为什么这可能是一个问题。

也许解决这个问题的方法是,如果您以这种方式分配许多较小的对象,您最好分配一大块内存并自己管理它。然后当你完成释放大块时。

于 2011-03-24T10:22:34.733 回答
1

我就这个问题与一些权威人士进行了交谈(格雷格,如果你在外面,打个招呼;D)并且可以确认大卫所说的基本上是正确的。

随着堆在分配 ~0.25MB 对象的第一遍中增长,堆正在保留和提交内存。当堆在删除过程中缩小时,它会以某种速度取消提交,但不一定释放它在分配过程中保留的虚拟地址范围。在最后一次分配过程中,1MB 分配由于其大小而绕过堆,因此开始与堆竞争 VA。

请注意,堆保留VA,而不是保持提交。 如果您好奇的话,VirtualAllocVirtualFree可以帮助解释不同之处。这个事实并不能解决您遇到的问题,即进程用尽了虚拟地址空间

于 2011-03-25T20:50:06.477 回答
0

这是低碎片堆的副作用。

http://msdn.microsoft.com/en-us/library/aa366750(v=vs.85).aspx

您应该尝试禁用它,看看是否有帮助。针对 GetProcessHeap 和 CRT 堆(以及您可能创建的任何其他堆)运行。

于 2011-03-25T02:26:39.103 回答