VC++多线程下内存操作的优化

原创 2000年12月12日 13:51:00

作者/李红亚<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

 

许多程序员发现用VC++编写的程序在多处理器的电脑上运行会变得很慢,这种情况多是由于多个线程争用同一个资源引起的。对于用VC++编写的程序,问题出在VC++的内存管理的具体实现上。以下通过对这个问题的解释,提供一个简便的解决方法,使得这种程序在多处理器下避免出现运行瓶颈。这种方法在没有VC++程序的源代码时也能用。

问题

    CC++运行库提供了对于堆内存进行管理的函数:C提供的是malloc()free()C++提供的是newdelete。无论是通过malloc()还是new申请内存,这些函数都是在堆内存中寻找一个未用的块,并且块的大小要大于所申请的大小。如果没有足够大的未用的内存块,运行时间库就会向操作系统请求新的页。页是虚拟内存管理器进行操作的单位,在基于Intel的处理器的NT平台下,一般是4,096字节。当你调用free()delete释放内存时,这些内存块就返还给堆,供以后申请内存时用。


    这些操作看起来不太起眼,但是问题的关键。问题就发生在当多个线程几乎同申请内存时,这通常发生在多处理器的系统上。但即使在一个单处理器的系统上,如果线程在错误的时间被调度,也可能发生这个问题。

考虑处于同一进程中的两个线程,线程1在申请1,024字节的内存的同时,运行于另外一个处理器的线程2申请256字节内存。内存管理器发现一个未用的内存块用于线程1,同时同一个函数发现了同一块内存用于线程2。如果两个线程同时更新内部数据结构,记录所申请的内存及其大小,堆内存就会产生冲突。即使申请内存的函数者成功返回,两个线程都确信自己拥有那块内存,这个程序也会产生错误,这只是个时间问题。

产生这种情况称为争用,是编写多线程程序的最大问题。解决这个问题的关键是要用一个锁定机制来保护内存管理器的这些函数,锁定机制保证运行相同代码的多个线程互斥地进行,如果一个线程正运行受保护的代码,则其他的线程都必须等待,这种解决方法也称作序列化。

    NT提供了一些锁定机制的实现方法。CreateMutex()创建一个系统范围的锁定对象,但这种方法的效率最低;InitializeCriticalSection()创建的critical section相对效率就要高许多;要得到更好的性能,可以用具有service pack 3NT 4spin lock,更详细的信息可以参考VC++帮助中的InitializeCriticalSectionAndSpinCount()函数的说明。有趣的是,虽然帮助文件中说spin lock用于NT的堆管理器(HeapAlloc()系列的函数)VC++运行库的堆管理函数并没有用spin lock来同步对堆的存取。如果查看VC++运行库的堆管理函数的源程序,会发现是用一个critical section用于全部的内存操作。如果可以在VC++运行库中用HeapAlloc(),而不是其自己的堆管理函数,将会因为使用的是spin lock而不是critical section而得到速度优化。

通过使用critical section同步对堆的存取,VC++运行库可以安全地让多个线程申请和释放内存。然而,由于内存的争用,这种方法会引起性能的下降。如果一个线程存取另外一个线程正在使用的堆时,前一个线程就需要等待,并丧失自己的时间片,切换到其他的线程。线程的切换在NT下是相当费时的,因为其占用线程的时间片的一个小的百分比。如果有多个线程同时要存取同一个堆,会引起更多的线程切换,足够引起极大的性能损失。

 

现象

    如何发现多处理器系统存在这种性能损失?有一个简便的方法,打开“管理工具”中的“性能”监视器,在系统组中添加一个上下文切换/秒计数,然后运行想要测试的多线程程序,并且在进程组中添加该进程的处理器时间计数,这样就可以得到处理器在高负荷下要发生多少次上下文切换。

在高负荷下有上千次的上下文切换是正常的,但当计数超过80,000100,000时,说明过多的时间都浪费在线程的切换,稍微计算一下就可以知道,如果每秒有100,000次线程切换,则每个线程只有10微秒用于运行,而NT上的正常的时间片长度约有12毫秒,是前者的上千倍。

    1的性能图显示了过度的线程切换,而图2显示了同一个进程在同样的环境下,在使用了下面提供的解决方法后的情况。图1的情况下,系统每秒钟要进行120,000次线程切换,改进后,每秒钟线程切换的次数减少到1,000次以下。两张图都是在运行同一个测试程序时截取得,程序中同时有3个线程同时进行最大为2048字节的堆的申请,硬件平台是一个双Pentium II 450机器,有256MB内存。

 

解决方法

本方法要求多线程程序是用VC++编写的,并且是动态链接到C运行库的。要求NT系统所安装的VC++运行库文件msvcrt.dll的版本号是6,所安装的service pack的版本是5以上。如果程序是用VC++ v6.0以上版本编译的,即使多线程程序和libcmt.lib是静态链接,本方法也可以使用。

    当一个VC++程序运行时,C运行库被初始化,其中一项工作是确定要使用的堆管理器,VC++ v6.0运行库既可以使用其自己内部的堆管理函数,也可以直接调用操作系统的堆管理函数(HeapAlloc()系列的函数),在__heap_select()函数内部分执行以下三个步骤:

    1、检查操作系统的版本,如果运行于NT,并且主版本是5或更高(Window 2000及以后版本),就使用HeapAlloc()

    2、查找环境变量__MSVCRT_HEAP_SELECT,如果有,将确定使用哪个堆函数。如果其值是__GLOBAL_HEAP_SELECTED,则会改变所有程序的行为。如果是一个可执行文件的完整路径,还要调用GetModuleFileName()检查是否该程序存在,至于要选择哪个堆函数还要查看逗号后面的值,1表示使用HeapAlloc()2表示使用VC++ v5的堆函数,3表示使用VC++ v6的堆函数。

    3、检测可执行文件中的链接程序标志,如果是由VC++ v6或更高的版本创建的,就使用版本6的堆函数,否则使用版本5的堆函数。

    那么如何提高程序的性能?如果是和msvcrt.dll动态链接的,保证这个dll19992月以后,并且安装的service pack的版本是5或更高。如果是静态链接的,保证链接程序的版本号是6或更高,可以用quickview.exe程序检查这个版本号。要改变所要运行的程序的堆函数的选取,在命令行下键入以下命令:

set __MSVCRT_HEAP_SELECT=__GLOBAL_HEAP_SELECTED,1

    以后,所有从这个命令行运行的程序,都会继承这个环境变量的设置。这样,在堆操作时都会使用HeapAlloc()。如果让所有的程序都使用这些速度更快的堆操作函数,运行控制面板的“系统”程序,选择“环境”,点取“系统变量”,输入变量名和值,然后按“应用”按钮关闭对话框,重新启动机器。

按照微软的说法,可能有一些用VC++ v6以前版本编译程序,使用VC++ v6的堆管理器会出现一些问题。如果在进行以上设置后遇到这样的问题,可以用一个批处理文件专门为这个程序把这个设置去掉,例如:

set __MSVCRT_HEAP_SELECT=c:/program files/myapp/myapp.exe,1 c:/bin/buggyapp.exe,2

 

测试

    为了验证在多处理器下的效果,编了一个测试程序heaptest.c。该程序接收三个参数,第一个参数表示线程数,第二个参数是所申请的内存的最大值,第三个参数每个线程申请内存的次数。

#define WIN32_LEAN_AND_MEAN

#include <windows.h>

#include <process.h>

#include <stdio.h>

#include <stdlib.h>

/* compile with cl /MT heaptest.c */

/* to switch to the system heap issue the following command

   before starting heaptest from the same command line

   set __MSVCRT_HEAP_SELECT=__GLOBAL_HEAP_SELECTED,1 */

/* structure transfers variables to the worker threads */

typedef struct tData

{

    int maximumLength;

    int allocCount;

} threadData;

void printUsage(char** argv)

{

    fprintf(stderr,"Wrong number of parameters./nUsage:/n");

    fprintf(stderr,"%s threadCount maxAllocLength allocCount/n/n",

        argv[0]);

    exit(1);

}

unsigned __stdcall workerThread(void* myThreadData)

{

    int count;

    threadData* myData;

    char* dummy;

    srand(GetTickCount()*GetCurrentThreadId());

    myData=(threadData*)myThreadData;

    /* now let us do the real work */

    for(count=0;count<myData->allocCount;count++)

    {

        dummy=(char*)malloc((rand()%myData->maximumLength)+1);

        free(dummy);

    }

    _endthreadex(0);

    /* to satisfy compiler */

    return 0;

}

int main(int argc,char** argv)

{

    int threadCount;

    int count;

    threadData actData;

    HANDLE* threadHandles;

    DWORD startTime;

    DWORD stopTime;

    DWORD retValue;

    unsigned dummy;

    /* check parameters */

    if(argc<4 || argc>4)

        printUsage(argv);

    /* get parameters for this run */

    threadCount=atoi(argv[1]);

    if(threadCount>64)

        threadCount=64;

    actData.maximumLength=atoi(argv[2])-1;

    actData.allocCount=atoi(argv[3]);

    threadHandles=(HANDLE*)malloc(threadCount*sizeof(HANDLE));

    printf("Test run with %d simultaneous threads:/n",threadCount);

    startTime=GetTickCount();

    for(count=0;count<threadCount;count++)

    {

        threadHandles[count]=(HANDLE)_beginthreadex(0,0,

            &workerThread, (void*)&actData,0,&dummy);

        if(threadHandles[count]==(HANDLE)-1)

        {

            fprintf(stderr,"Error starting worker threads./n");

            exit(2);

        }

    }

    /* wait until all threads are done */

    retValue=WaitForMultipleObjects(threadCount,threadHandles

        ,1,INFINITE);

    stopTime=GetTickCount();

    printf("Total time elapsed was: %d milliseconds",

        stopTime-startTime);

    printf(" for %d alloc operations./n",

        actData.allocCount*threadCount);

    /* cleanup */

    for(count=0;count<threadCount;count++)

        CloseHandle(threadHandles[count]);

    free(threadHandles);

    return 0;

}

测试程序在处理完参数后,创建参数1指定数量的线程,threadData结构用于传递计数变量。workThread中进行内存操作,首先初始化随机数发生器,然后进行指定数量的malloc()free()操作。主线程调用WaitForMultipleObject()等待工作者线程结束,然后输出线程运行的时间。计时不是十分精确,但影响不大。

    为了编译这个程序,需要已经安装VC++ v6.0程序,打开一个命令行窗口,键入以下命令:

cl /MT heaptest.c

    /MT表示同C运行库的多线程版静态链接。如果要动态链接,用/MD。如果VC++v5.0的话并且有高版本的msvcrt.dll,应该用动态链接。现在运行这个程序,用性能监视器查看线程切换的次数,然后按上面设置环境参数,重新运行这个程序,再次查看线程切换次数。

    当截取这两张图时,测试程序用了60,953ms进行了3,000,000次的内存申请操作,使用的是VC++ v6的堆操作函数。在转换使用HeapAlloc()后,同样的操作仅用了5,291ms。在这个特定的情况下,使用HeapAlloc()使得性能提高了10倍以上!在实际的程序同样可以看到这种性能的提升。

   

结论

多处理器系统可以自然提升程序的性能,但如果发生多个处理器争用同一个资源,则可能多处理器的系统的性能还不如单处理器系统。对于C/C++程序,问题通常发生在当多个线程进行频繁的内存操作活动时。如上文所述,只要进行很少的一些设置,就可能极大地提高多线程程序在多处理器下的性能。这种方法即不需要源程序,也不需要重新编译可执行文件,而最大的好处是用这种方法得到的性能的提高是不用支付任何费用的。

VC使用EmptyWorkingSet()来清理内存

360安全卫士有清理内存的工具,qq管家却没有,不过为了qq升级~唉只好用qq管家,可是电脑总休眠就会造成内存占用过多,甚至90%多,电脑运行速度直线下降,所以想自己写个小工具清理下内存,根据以前用超...
  • chenlycly
  • chenlycly
  • 2014年04月14日 22:10
  • 5834

VS调试程序_ASSERTE(_CrtIsValidHeapPointer(pUserData))崩溃的原因以及解决方法

调试程序,对动态申请的内存用free或者delete释放时程序崩溃,跳出如下对话框: 点击重试,定位到具体的CRT源码部分:_ASSERTE(_CrtIsValidHeapPointer(p...
  • mfcing
  • mfcing
  • 2015年01月22日 17:53
  • 11105

c++五种内存分配、堆与栈区别

在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。     栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常...
  • dapengbusi
  • dapengbusi
  • 2015年10月12日 10:14
  • 5388

VC++动态分配内存出错解决方法

问题描述: 我做了一个tcp发包和接包的函数,但是运行之后,总是报错,错误如下:     我的函数代码如下: void CThreadTestDlg::sendmsg() thr...
  • superwang007
  • superwang007
  • 2014年08月28日 17:09
  • 344

VC中用内存映射文件处理大文件

文件操作是应用程序最为基本的功能之一,Win32 API和MFC均提供有支持文件处理的函数和类,常用的有Win32 API的CreateFile()、WriteFile()、ReadFile()和MF...
  • zhyi0000
  • zhyi0000
  • 2015年04月26日 23:40
  • 601

VC++内存申请

问题:如果要申请一块内存?应该如何做?       解答:在Vc++一般的做法是:              LPVOID pvData;//声明一个任何类型的指针      ...
  • txk15619567977
  • txk15619567977
  • 2013年05月24日 08:47
  • 2004

VC++自释放指针、自释放虚拟内存、自关闭句柄、局部作用域回调函数调用等辅助开发类

VC++自释放指针、自释放虚拟内存、自关闭句柄、局部作用域回调函数调用等辅助开发类...
  • sdhongjun
  • sdhongjun
  • 2014年12月21日 10:15
  • 1935

VC 检测内存泄露的几种方法

在Visual C++中检测和隔离内存泄漏 具有动态的分配和释放内存的能力是C/C++程序语言的重要特色之一。VisualC++ debugger和CRT库提供了一系列有效的检测和鉴定内存泄漏的...
  • xiliang_pan
  • xiliang_pan
  • 2014年07月11日 11:42
  • 13905

VC 共享内存实现进程间大数据的交换

作者:中国电波传播研究所青岛分所:郎锐     在Windows操作系统下,任何一个进程不允许读取、写入或是修改另一个进程的数据(包括变量、对象和内存分配等),但是在某个进程内创建的文件映射对象的视图...
  • element_storm
  • element_storm
  • 2013年11月21日 10:48
  • 2148

vc++6.0查看程序中内存

学过单片机的都知道,在keil中编程时查看程序内存是调试的重要手段之一,vc++6.0也不例外。言归正传,下面讲解一下如何在vc++6.0中查看内存。 这是我写的一段调试程序 #include...
  • MyLinChi
  • MyLinChi
  • 2016年09月25日 13:39
  • 2475
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:VC++多线程下内存操作的优化
举报原因:
原因补充:

(最多只允许输入30个字)