new和delete[]比malloc和free更糟糕吗?(C++/VS2012)

9

好的,我写了一些代码来检查运行时可用的内存。下面是一个完整(最小)的cpp文件。

注意:这段代码不完美,也不是最佳实践,但我希望你能关注内存管理而不是代码。

它的功能(第一部分):

  • (1) 在一个块中分配尽可能多的内存。清除该内存
  • (2) 分配尽可能多的中等大小的块(16MB)。清除该内存。

--> 这很好用

它的功能(第二部分):

  • (1) 在一个块中分配尽可能多的内存。清除该内存
  • (2) 分配尽可能多的微小块(16kb)。清除该内存。

--> 这个表现很奇怪!

问题是:如果我重复这样做,我只能为第二次运行分配522kb --->?

如果分配的块有16MB大小,就不会发生这种情况。

你有任何想法,为什么会发生这种情况吗?

// AvailableMemoryTest.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"

#include <vector>
#include <list>
#include <limits.h>
#include <iostream>


int _tmain(int argc, _TCHAR* argv[])
{


    auto determineMaxAvailableMemoryBlock = []( void ) -> int
    {
        int nBytes = std::numeric_limits< int >::max();

        while ( true )
        {
            try
            {
                std::vector< char >vec( nBytes );
                break;
            }
            catch ( std::exception& ex )
            {
                nBytes = static_cast< int >( nBytes * 0.99 );
            }
        }
        return nBytes;
    };

    auto determineMaxAvailableMemoryFragmented = []( int nBlockSize ) -> int
    {

        int nBytes = 0;

        std::list< std::vector< char > > listBlocks;

        while ( true )
        {
            try
            {
                listBlocks.push_back( std::vector< char >( nBlockSize ) );
                nBytes += nBlockSize;
            }
            catch ( std::exception& ex )
            {
                break;
            }
        }
        return nBytes;
    };


    std::cout << "Test with large memory blocks (16MB):\n";
    for ( int k = 0; k < 5; k++ )
    {
        std::cout << "run #" << k << "   max  mem block          = " << determineMaxAvailableMemoryBlock() / 1024.0 / 1024.0 << "MB\n";
        std::cout << "run #" << k << "   frag mem blocks of 16MB = " << determineMaxAvailableMemoryFragmented( 16*1024*1024 ) / 1024.0 / 1024.0 << "MB\n";
        std::cout << "\n";
    } // for_k
    

    std::cout << "Test with small memory blocks (16k):\n";
    for ( int k = 0; k < 5; k++ )
    {
        std::cout << "run #" << k << "   max  mem block          = " << determineMaxAvailableMemoryBlock() / 1024.0 / 1024.0 << "MB\n";
        std::cout << "run #" << k << "   frag mem blocks of 16k  = " << determineMaxAvailableMemoryFragmented( 16*1024 ) / 1024.0 / 1024.0 << "MB\n";
        std::cout << "\n";
    } // for_k

    std::cin.get();


    return 0;
}

使用大内存块输出(这很好用)

Test with large memory blocks (16MB):
run #0   max  mem block          = 1023.67MB     OK
run #0   frag mem blocks of 16MB = 1952MB        OK

run #1   max  mem block          = 1023.67MB     OK
run #1   frag mem blocks of 16MB = 1952MB        OK

run #2   max  mem block          = 1023.67MB     OK
run #2   frag mem blocks of 16MB = 1952MB        OK

run #3   max  mem block          = 1023.67MB     OK
run #3   frag mem blocks of 16MB = 1952MB        OK

run #4   max  mem block          = 1023.67MB     OK
run #4   frag mem blocks of 16MB = 1952MB        OK

使用小内存块进行输出(从第二次运行开始,内存分配很奇怪)

Test with small memory blocks (16k):
run #0   max  mem block          = 1023.67MB     OK
run #0   frag mem blocks of 16k  = 1991.06MB     OK

run #1   max  mem block          = 0.493021MB    ???
run #1   frag mem blocks of 16k  = 1991.34MB     OK

run #2   max  mem block          = 0.493021MB    ???
run #2   frag mem blocks of 16k  = 1991.33MB     OK

run #3   max  mem block          = 0.493021MB    ???
run #3   frag mem blocks of 16k  = 1991.33MB     OK

run #4   max  mem block          = 0.493021MB    ???
run #4   frag mem blocks of 16k  = 1991.33MB     OK

更新:

这个问题也会出现在使用new和delete[]而不是STL内部的内存分配。

更新:

它可以在64位上工作(我限制了两个函数允许分配的内存为12GB)。真的很奇怪。这是该版本RAM使用情况的图像:

RAM usage

更新:

它可以使用malloc和free,但不能使用new和delete[](或者如上所述的STL)。


1
一种可能性:堆管理器往往针对不同的块大小有各种不同的堆区域。当您首先用小块填充所有内容时,其中一个小块堆区域可能会占据所有空间,并保留以供将来分配使用。 - Sebastian Redl
好的,但为什么它会保留内存而不是将其分配给下一个?它已经不再使用了... - S.H
1
你为什么要创建 std::vector<char> 而不是更直接的方法(new char[]malloc)? - sfstewman
1
@sfstewman:为什么不呢?这是最简单的方法,而且在功能上等同于您的hacky、低级、容易出错的方法。例如,这种方法避免了任何担心分配的块可能泄漏的情况。OP做得很对。 - Lightness Races in Orbit
2
这很可能是碎片化的结果。并非所有堆实现都能很好地管理碎片化。当您分配小块并将它们返回到堆时,它们可能不会合并,从而有效地减少可用内存。您可以尝试使用不同的堆实现,例如tcmalloc,看看是否会得到不同的结果。 - Jason
显示剩余3条评论
2个回答

3
正如我在上面的评论中提到的那样,这很可能是一个堆 碎片 问题。堆将维护不同大小的块列表以满足不同的内存请求。较大的内存块被分成较小的块,以满足较小的内存请求,以避免浪费块大小和请求大小之间的差异,从而减少了较大块的数量。因此,当请求较大的块时,堆可能没有足够的大块来满足请求。
由于碎片化会有效地减少可用内存,因此堆实现中的碎片化是一个主要问题。然而,一些堆实现能够将较小的块合并回较大的块,并且即使经过多次较小的请求后,它们也更能够满足大的请求。
我使用glibc的mallocptmalloc)运行了您上面的代码,稍微进行了修改,结果如下...
Test with large memory blocks (16MB):
run #0   max  mem block          = 2048MB
run #0   frag mem blocks of 16MB = 2032MB

run #1   max  mem block          = 2048MB
run #1   frag mem blocks of 16MB = 2032MB

run #2   max  mem block          = 2048MB
run #2   frag mem blocks of 16MB = 2032MB

run #3   max  mem block          = 2048MB
run #3   frag mem blocks of 16MB = 2032MB

run #4   max  mem block          = 2048MB
run #4   frag mem blocks of 16MB = 2032MB

Test with small memory blocks (16k):
run #0   max  mem block          = 2048MB
run #0   frag mem blocks of 16k  = 2047.98MB

run #1   max  mem block          = 2048MB
run #1   frag mem blocks of 16k  = 2047.98MB

run #2   max  mem block          = 2048MB
run #2   frag mem blocks of 16k  = 2047.98MB

run #3   max  mem block          = 2048MB
run #3   frag mem blocks of 16k  = 2047.98MB

run #4   max  mem block          = 2048MB
run #4   frag mem blocks of 16k  = 2047.98MB

所以,ptmalloc 至少在这种特定情况下似乎能够很好地处理碎片化问题。

分段意味着,如果我分配块 ABCDE,然后删除 ACE,则会得到 xBxDx,我无法分配大于 A、C 或 E 的块。但是我正在做的是删除所有块,所以我应该得到完全空的 xxxxx 内存,并且重新分配应该有效! - S.H
问题在于堆不一定知道 ABCDE 是连续的。例如,如果 ABCDE 被分成 A、B、C、D 和 E。如果先返回 A,然后是 B,堆不一定知道 B 是 A 后面的地址(除非堆像 ptmalloctcmalloc 一样设计)。你能试着链接到 tcmalloc 吗?根据这个线程,你只需要在 MSVCRT 之前链接到 tcmalloc 就可以了。 - Jason
问题在于堆不一定知道 ABCDE 是连续的。这将是堆实现中的致命错误,使其无法用于任何真正的工作。 - n. m.
并非所有堆实现都会积极合并相邻的块,甚至根本不合并相邻的块。我知道的唯一使用基数树来维护跨度的是“tcmalloc”。 - Jason

1
我猜你正在运行32位代码,有些原因吗?
最好的猜测是:你的malloc实现在释放后将记账数据散布在内存中的各个地方,当分配很小的时候。malloc实现通常为小型和大型分配使用不同的分配策略,因此记账信息可能在不同的位置上并不奇怪。
在Unix上,小型分配通常使用brk,而大型分配则使用mmap(MAP_ANONYMOUS)。_tmain和stdafx.h意味着你正在Windows上测试,所以我不知道该malloc在底层如何工作。

1
@LightnessRacesinOrbit:即使当前的Atom CPU也支持64位模式。x86-32已经过时多年了。额外的寄存器和寄存器调用ABI在典型代码上可以提高约10到15%的性能。如果您正在编写新代码,应该使用64位。如果您要制作32位版本,那只是为了支持硬件非常差的用户。 - Peter Cordes
2
好吧,请开放你的思维;我们并不都是为消费市场编写全新的商品硬件。你认为基础设施是从哪里来的? - Lightness Races in Orbit
@LightnessRacesinOrbit:当然,制作32位二进制文件也有其原因。但在我看来,“默认”应该是64位。 - Peter Cordes
我会检查64位版本并发布更新...然而...这真的不应该有影响。我的系统应该能够重新分配内存! - S.H
@S.H:是的,您使用的32位构建中的malloc明显存在问题。(可能是默认的Visual C++ malloc。)同一个测试程序的64位构建将在遇到问题之前耗尽虚拟内存。我的观点是,64位代码首先不会有接近耗尽地址空间的问题。只有在需要坚如磐石的32位软件版本时才需要处理此问题。这可能是还可能不是情况。 - Peter Cordes
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接