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

简介:

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

问题

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

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

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

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

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

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

现象

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

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

性能图显示了过度的线程切换,而图2显示了同一个进程在同样的环境下,在使用了下面提供的解决方法后的情况。系统每秒钟要进行120,000次线程 切换,改进后,每秒钟线程切换的次数减少到1,000次以下。两张图都是在运行同一个测试程序时截取得,程序中同时有3个线程同时进行最大为2,048字 节的堆的申请,硬件平台是一个双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动态链接的,保证这个dll是1999年2月以后,并且安装的service pack的版本是5或更高。如果是静态链接的,保证链接程序的版本号是6或更高,可以用quickview.exe程序检查这个版本号。要改变所要运行的 程序的堆函数的选取,在命令行下键入以下命令:

  1. set __MSVCRT_HEAP_SELECT=__GLOBAL_HEAP_SELECTED,1

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

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

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

测试

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

  1. #define WIN32_LEAN_AND_MEAN
  2. #include <windows.h> 
  3. #include <process.h>
  4. #include <stdio.h>
  5. #include <stdlib.h>
  6.  
  7.  // compile with cl /MT heaptest.c
  8.  /* to switch to the system heap issue the following command
  9. before starting heaptest from the same command line
  10. set __MSVCRT_HEAP_SELECT=__GLOBAL_HEAP_SELECTED,1 */
  11.  //structure transfers variables to the worker threads
  12.  
  13. typedef struct tData {
  14.    int maximumLength;
  15.   int allocCount;
  16. } threadData;
  17.  
  18. void printUsage(char** argv) {
  19.   fprintf(stderr,"Wrong number of parameters./nUsage:/n");
  20.   fprintf(stderr,"%s threadCount maxAllocLength allocCount/n/n", argv[0])
  21.   exit(1);
  22. } 
  23.  
  24. unsigned __stdcall workerThread(void* myThreadData) {
  25.   int count;
  26.   threadData* myData;
  27.   char* dummy;
  28.   srand(GetTickCount()*GetCurrentThreadId());
  29.  //now let us do the real work
  30.   myData=(threadData*)myThreadData;
  31.  
  32.   for (count=0;countallocCount;count++) { 
  33.     dummy=(char*)malloc((rand()%myData->maximumLength)+1);
  34.     free(dummy);
  35.   }
  36. //to satisfy compiler/
  37.   _endthreadex(0);
  38.   return 0;
  39. }
  40.  
  41. int main(int argc,char** argv) { 
  42.   int threadCount;
  43.   int count;
  44.   threadData actData;
  45.   HANDLE* threadHandles;
  46.   DWORD startTime;
  47.   DWORD stopTime;
  48.   DWORD retValue;
  49.  
  50.     // check parameters
  51.   unsigned dummy;
  52.     // get parameters for this run
  53.   if (argc<4 || argc>4) printUsage(argv);
  54.   threadCount=atoi(argv[1]);
  55.   if (threadCount>64) threadCount=64;
  56.   actData.maximumLength=atoi(argv[2])-1;
  57.   actData.allocCount=atoi(argv[3]);
  58.   threadHandles=(HANDLE*)malloc(threadCount*sizeof(HANDLE));
  59.   printf("Test run with %d simultaneous threads:/n",threadCount);
  60.   startTime=GetTickCount();
  61.   for(count=0;count<threadCount;count++)
  62.     {
  63.         threadHandles[count]=(HANDLE)_beginthreadex(0,0,
  64.             &workerThread, (void*)&actData,0,&dummy);
  65.         if (threadHandles[count]==(HANDLE)-1)
  66.         {
  67.             fprintf(stderr,"Error starting worker threads./n");
  68.             exit(2);
  69.         }
  70.     }
  71.  
  72.     // wait until all threads are done
  73.     retValue=WaitForMultipleObjects(threadCount,threadHandles,
  74.                 1,INFINITE);
  75.     stopTime=GetTickCount();
  76.     printf("Total time elapsed was: %d milliseconds",
  77.         stopTime-startTime);
  78.     printf(" for %d alloc operations./n",
  79.         actData.allocCount*threadCount);
  80.  
  81.     // cleanup
  82.     for(count=0;count<threadCount;count++)
  83.         CloseHandle(threadHandles[count]);
  84.     free(threadHandles);
  85.     return 0;
  86. }

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

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

  1. 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++程序,问题通 常发生在当多个线程进行频繁的内存操作活动时。如上文所述,只要进行很少的一些设置,就可能极大地提高多线程程序在多处理器下的性能。这种方法即不需要源 程序,也不需要重新编译可执行文件,而最大的好处是用这种方法得到的性能的提高是不用支付任何费用的。

 
目录
相关文章
|
3月前
|
C语言
C语言内存及堆栈操作
C语言内存及堆栈操作
19 0
|
6月前
|
Oracle 关系型数据库 Linux
解决在linux服务器上部署定时自动查找cpu,内存,磁盘使用量,并将查询结果写入数据库的脚本,只能手动运行实现插库操作
问题描述:将脚本名命名为mortior.sh(以下简称mo),手动执行脚本后查询数据库,表中有相应的信息,放入自动执行队列中,脚本被执行,但是查询数据库,并没有新增数据。
46 0
|
6月前
|
开发框架 算法 .NET
一个简单高效低内存的.NET操作Excel开源框架 - MiniExcel
一个简单高效低内存的.NET操作Excel开源框架 - MiniExcel
|
6月前
|
缓存 开发框架 算法
.NET操作Excel高效低内存的开源框架 - MiniExcel
.NET操作Excel高效低内存的开源框架 - MiniExcel
61 0
.NET操作Excel高效低内存的开源框架 - MiniExcel
|
10月前
|
存储 关系型数据库 API
应用PMDK修改WAL操作使之适配持久化内存
应用PMDK修改WAL操作使之适配持久化内存
77 0
|
12月前
|
Java
Java 二维数组格式、二维数组内存图解、二维数组操作
Java 二维数组格式、二维数组内存图解、二维数组操作
115 0
|
算法 Java PHP
PHP是如何操作内存的?底层原理是什么?
PHP是如何操作内存的?底层原理是什么?
107 0
|
C语言
C语言内存及堆栈操作
C语言内存及堆栈操作
90 0
C语言内存及堆栈操作
FreeType 使用FT_MEM_ALLOC/FT_FREE内存操作
FreeType 使用FT_MEM_ALLOC/FT_FREE内存操作
77 0
|
C语言 C++ 芯片
C语言熟练操作内存
C语言熟练操作内存
112 0