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. unsigned __stdcall workerThread(void* myThreadData) {
  24.   int count;
  25.   threadData* myData;
  26.   char* dummy;
  27.   srand(GetTickCount()*GetCurrentThreadId());
  28.  //now let us do the real work
  29.   myData=(threadData*)myThreadData;
  30.  
  31.   for (count=0;countallocCount;count++) { 
  32.     dummy=(char*)malloc((rand()%myData->maximumLength)+1);
  33.     free(dummy);
  34.   }
  35. //to satisfy compiler/
  36.   _endthreadex(0);
  37.   return 0;
  38. }
  39.  
  40. int main(int argc,char** argv) { 
  41.   int threadCount;
  42.   int count;
  43.   threadData actData;
  44.   HANDLE* threadHandles;
  45.   DWORD startTime;
  46.   DWORD stopTime;
  47.   DWORD retValue;
  48.  
  49.     // check parameters
  50.   unsigned dummy;
  51.     // get parameters for this run
  52.   if (argc<4 || argc>4) printUsage(argv);
  53.   threadCount=atoi(argv[1]);
  54.   if (threadCount>64) threadCount=64;
  55.   actData.maximumLength=atoi(argv[2])-1;
  56.   actData.allocCount=atoi(argv[3]);
  57.   threadHandles=(HANDLE*)malloc(threadCount*sizeof(HANDLE));
  58.   printf("Test run with %d simultaneous threads:/n",threadCount);
  59.   startTime=GetTickCount();
  60.   for(count=0;count<threadCount;count++)
  61.     {
  62.         threadHandles[count]=(HANDLE)_beginthreadex(0,0,
  63.             &workerThread, (void*)&actData,0,&dummy);
  64.         if (threadHandles[count]==(HANDLE)-1)
  65.         {
  66.             fprintf(stderr,"Error starting worker threads./n");
  67.             exit(2);
  68.         }
  69.     }
  70.  
  71.     // wait until all threads are done
  72.     retValue=WaitForMultipleObjects(threadCount,threadHandles,
  73.                 1,INFINITE);
  74.     stopTime=GetTickCount();
  75.     printf("Total time elapsed was: %d milliseconds",
  76.         stopTime-startTime);
  77.     printf(" for %d alloc operations./n",
  78.         actData.allocCount*threadCount);
  79.  
  80.     // cleanup
  81.     for(count=0;count<threadCount;count++)
  82.         CloseHandle(threadHandles[count]);
  83.     free(threadHandles);
  84.     return 0;
  85. }

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

 

时间: 2024-10-31 14:21:36

VC++多线程下内存操作的优化的相关文章

Java中对AtomicInteger和int值在多线程下递增操作的测试_java

Java针对多线程下的数值安全计数器设计了一些类,这些类叫做原子类,其中一部分如下: java.util.concurrent.atomic.AtomicBoolean; java.util.concurrent.atomic.AtomicInteger; java.util.concurrent.atomic.AtomicLong; java.util.concurrent.atomic.AtomicReference; 下面是一个对比  AtomicInteger 与 普通 int 值在多线

共享内存操作类(C#源码)

  原文 http://blog.csdn.net/yefanqiu/article/details/1717458   VC++的共享内存操作代码实现起来相对比较容易,但是用C#语言来实现,就有一定难度,由于工作需要,把以前VC开发的共享内存代码要用C#实现,别说,还费了不少周折,毕竟C#操作API函数和地址指针不是那么直接,还好,总算完成了,效果还不错.      共享内存操作类:     using System;using System.Collections.Generic;using

Java I/O 操作及优化建议

Java I/O I/O,即 Input/Output(输入/输出) 的简称.就 I/O 而言,概念上有 5 种模型:blocking I/O,nonblocking I/O,I/O multiplexing (select and poll),signal driven I/O (SIGIO),asynchronous I/O (the POSIX aio_functions).不同的操作系统对上述模型支持不同,UNIX 支持 IO 多路复用.不同系统叫法不同,freebsd 里面叫 kque

C语言嵌入式系统编程修炼(内存操作)

在嵌入式系统的编程中,常常要求在特定的内存单元读写内容,汇编有对应的MOV指令,而除C/C++以外的其它编程语言基本没有直接访问绝对地址的能力.在嵌入式系统的实际调试中,多借助C语言指针所具有的对绝对地址单元内容的读写能力.以指针直接操作内存多发生在如下几种情况: (1) 某I/O芯片被定位在CPU的存储空间而非I/O空间,而且寄存器对应于某特定地址: (2) 两个CPU之间以双端口RAM通信,CPU需要在双端口RAM的特定单元(称为mail box)书写内容以在对方CPU产生中断: (3) 读

VC多线程编程详解_C 语言

本文实例讲述了VC多线程编程概念与技巧,分享给大家供大家参考.具体分析如下: 一.多线程编程要点 线程是进程的一条执行路径,它包含独立的堆栈和CPU寄存器状态,每个线程共享所有的进程资源,包括打开的文件.信号标识及动态分配的内存等.一个进程内的所有线程使用同一个地址空间,而这些线程的执行由系统调度程序控制,调度程序决定哪个线程可执行以及什么时候执行线程.线程有优先级别,优先权较低的线程必须等到优先权较高的线程执行完后再执行.在多处理器的机器上,调度程序可将多个线程放到不同的处理器上去运行,这样可

Java基础-22总结登录注册IO版,数据操作流,内存操作流,打印流,标准输入输出流,转换流,随机访问流,合并流,序列化流,Properties

你需要的是什么,直接评论留言. 获取更多资源加微信公众号"Java帮帮" (是公众号,不是微信好友哦) 还有"Java帮帮"今日头条号,技术文章与新闻,每日更新,欢迎阅读 学习交流请加Java帮帮交流QQ群553841695 分享是一种美德,分享更快乐! 1:登录注册IO版本案例(掌握) 要求,对着写一遍. cn.itcast.pojo User cn.itcast.dao UserDao cn.itcast.dao.impl UserDaoImpl(实现我不管)

cpu_relax( )-----对自选循环等待(spin-wait loops)操作的优化【转】

cpu_relax()-----对自选循环等待(spin-wait loops)操作的优化 转自:http://www.doc100.net/bugs/t/173547/index.html    在lock_timer_base()函数中看到在for循环操作中调用了cpu_relax(),本来以为是要让出CPU,调度其他进程运行,但是看代码之后发现完全不是这么回事.cpu_relax()中只有一条调用语句,调用的是rep_nop函数.rep_nop()函数如下:   static inline

如何使用线程局部存储实现多线程下的日志系统

概述 通常来说,在应用程序中需要日志来记录程序运行的状态,以便后期问题的跟踪定位.在日志系统的设计中,通常会有一个总的日志系统来统一协调这些日志的设置如位置.输出级别和内容等.在多线程编程中,当每个线程都需要输出日志时,因为要考虑线程间的同步,日志系统的设计更加复杂. 在单线程应用程序中,通常使用一个日志单例向某个文件输出应用运行过程中的重要日志信息,但是在多线程环境中,这样做显然不好,因为各个线程打印出的日志会错综复杂从而使得日志文件不容易阅读和跟踪.比较好的办法是主线程记录自己的日志,各个子

Python多线程和队列操作实例

  这篇文章主要介绍了Python多线程和队列操作实例,本文直接给给实例代码,需要的朋友可以参考下 Python3,开一个线程,间隔1秒把一个递增的数字写入队列,再开一个线程,从队列中取出数字并打印到终端 代码如下: #! /usr/bin/env python3 import time import threading import queue # 一个线程,间隔一定的时间,把一个递增的数字写入队列 # 生产者 class Producer(threading.Thread): def __i