VS2012编译器存在奇怪的内存释放问题

6

我在使用VS2012编译器时遇到了一个奇怪的问题,这个问题在GCC中似乎没有出现过。释放内存的过程需要花费数分钟而不是几秒钟。有人对此有什么想法吗?在调试过程中,发现在调用RtlpCollectFreeBlocks()时会明显卡顿。我在debug和release模式下都遇到了这个问题。我的操作系统是Windows 7 32位,但我在64位的7上也遇到了同样的问题。

#include "stdafx.h"
#include <iostream>
#include <stdint.h>
#include <cstdlib>

#define SIZE 500000

using namespace std;

typedef struct
{
    uint32_t* thing1;
}collection;

/*
 * VS2012 compiler used.
 * Scenarios: 
 *  1) Don't allocate thing1. Program runs poorly.
 *  2) Allocate thing1 but don't delete it. Program runs awesome.
 *  3) Allocate thing1 and delete it. Program runs poorly.
 * 
 * Debug or Release mode does not affect outcome. GCC's compiler is fine.
 */
int _tmain(int argc, _TCHAR* argv[])
{
    collection ** colArray = new collection*[SIZE];

    for(int i=0;i<SIZE;i++)
    {
        collection * mine = new collection;
        mine->thing1 = new uint32_t; // Allocating without freeing runs fine. Either A) don't allocate or B) allocate and delete to make it run slow.
        colArray[i] = mine;
    }

    cout<<"Done with assignment\n";

    for(int i=0;i<SIZE;i++)
    {
        delete(colArray[i]->thing1); // delete makes it run poorly.
        delete(colArray[i]);

        if(i > 0 && i%100000 == 0)
        {
            cout<<"100 thousand deleted\n";
        }
    }
    delete [] colArray;

    cout << "Done!\n";
    int x;
    cin>>x;
}

这只是一个简单的例子。我最初在结构体中有4个uint32_t。我的想法是要表明,结构体的大小似乎对这个问题并不重要。 - Sean
3
这似乎与 IDE 钩子相关。切换到命令提示符并从控制台运行您的程序。一切都没有问题,内存模型和调试状态也没有影响。 - WhozCraig
我的两分钱,VS2005之前,我们可以在VC++中使用单线程运行时库,在VS2005之后,我们只能使用多线程运行时库,这由/MT或/MD指定。当时我们调试代码时发现,由于同步问题,多线程运行时库中的free()函数比单线程运行时库中的慢。我不熟悉GCC。GCC编译的程序是否使用与VS中/MD或/MT等效的选项? - Matt
这个网址是否有帮助?https://dev59.com/Y3A75IYBdhLWcg3wUHSQ#4375879 - Mike Vine
1
请提供 https://dev59.com/sFjUa4cB1Zd3GeqPTKTr 的相关内容。 - Mike Vine
显示剩余5条评论
1个回答

8
你看到的性能下降是来自于Windows调试堆功能,即使在发布版本中也会启用,而且它的启用方式有点隐秘。我利用一个简单程序构建了64位的调试镜像并发现了以下内容:
- msvcr110d.dll!_CrtIsValidHeapPointer(const void * pUserData=0x0000000001a8b540) - msvcr110d.dll!_free_dbg_nolock(void * pUserData=0x0000000001a8b540, int nBlockUse=1) - msvcr110d.dll!_free_dbg(void * pUserData=0x0000000001a8b540, int nBlockUse=1) - msvcr110d.dll!operator delete(void * pUserData=0x0000000001a8b540)
对我特别感兴趣的是 `msvcr110d.dll!_CrtIsValidHeapPointer` 的主体部分,它的代码如下:
if (!pUserData)
    return FALSE;

// Note: all this does is checks for null    
if (!_CrtIsValidPointer(pHdr(pUserData), sizeof(_CrtMemBlockHeader), FALSE))
    return FALSE;

// but this is e-x-p-e-n-s-i-v-e
return HeapValidate( _crtheap, 0, pHdr(pUserData) );

那个 HeapValidate() 调用太过严厉了。
好吧,也许我会在调试版本中期望这样,但肯定不是在发布版本中。事实证明,情况有所改善,但看看调用堆栈:
- ntdll.dll!RtlDebugFreeHeap() - ntdll.dll!string "Enabling heap debug options\n"() - ntdll.dll!RtlFreeHeap() - kernel32.dll!HeapFree() - msvcr110.dll!free(void * pBlock)
很有趣,因为当我第一次运行它,然后使用IDE(或WinDbg)附加到正在运行的进程而不允许其控制执行启动环境时,此调用堆栈停在 ntdll.dll!RtlFreeHeap()。换句话说,在IDE之外运行RtlDebugFreeHeap不会被调用。但是为什么呢?
我想,也许调试器会开关以启用堆调试。经过一番挖掘,我发现“开关”就是调试器本身,如果被调试的进程是由调试器生成的,则Windows使用特殊的调试堆函数(RtlDebugAllocHeapRtlDebugFreeHeap)。来自MSDN的WinDbg的这篇文章给出了这个线索以及有关Windows下调试的其他有趣信息:

来自 使用WinDbg调试用户模式进程

调试器创建的进程(也称为派生进程)与调试器未创建的进程略有不同。

调试器创建的进程不使用标准堆API,而是使用特殊的调试堆。您可以通过使用_NO_DEBUG_HEAP环境变量或-hd命令行选项强制一个生成的进程使用标准堆而不是调试堆。

现在我们找对了方向。为了测试这一点,我简单地加入了一个 sleep(),让它运行一段时间,等待我附加调试器,而不是用调试器生成进程,然后让它开心地运行。果然,如前所述,它全速前进。
基于那篇文章的内容,我已经更新了我的Release模式构建,在项目文件的执行环境设置中定义了 _NO_DEBUG_HEAP=1。显然,我仍然对调试版本中的粒度堆活动感兴趣,所以这些配置保持不变。在这样做之后,我的Release构建在VS2012(和VS2010)下运行的速度明显快了很多,我邀请您也来尝试一下。

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