易语言教程_易语言源码_易语言视频教程_易语言论坛

 找回密码
 点击注册

Vip新手入门区
新手学习指南  学员作品展示 Vip课程总纲  Vip绝密课程系列

Vip相关下载区
Vip模块下载   Vip模块绑定   Vip模块例子 魔鬼插件下载  魔鬼插件例子  教程工具下载

Vip论坛服务区
教程问题提问区   模块问题提问区 技术交流区   魔鬼插件建议   忘记密码找回

VIP会员办理QQ: 8643245   
【请先加好友,然后到好友列表双击联系客服,办理VIP会员。】
【基础篇】易语言辅助入门基础教程
VIP模块办理QQ: 7189694 办理正版魔鬼作坊VIP模块 【基础篇】OD与CE入门基础教程
办理【终身VIP会员】“秒杀价” 仅需 RMB278.00元… 【基础篇】零基础绝密汇编语言入门课程 (共26课已完成)…
办理VIP详情…猛击这里查看详情 【基础篇】VIP辅助入门基础教程-新手必学 已发布10课 ……
VIP教程免费试看章节…猛击下载 【第1款】制作“辅助挂”教程目录查看(共107+16_x64下更新课已完成)…
亲爱的VIP学员,请到此写下你学习的感受与发布作品截图… 【第2款】制作“任务挂”教程目录查看(共77+1_x64下更新课已完成)…
卍解吧!不用bp send类封包断点找CALL的各种通杀思路 【第3款】驱动过保护技术课程(共38课已完成)…
【绝密教程】VIP绝密教程系列---注意:随时会更新! 【第4款】VIP邪恶二叉树辅助课程 (共31+17_x64下更新课已完成)…
【精品第13款】3D射击游戏与页游透视 智辅课程 已完成17课… 【第5款】零基础易语言按键辅助教程 (30课已完成)…
【精品第14款】变态功能辅助是如何炼成的 已完成36课… 【第6款】从零开始学习封包辅助技术教程(20课已完成) …
【精品第15款】DNF商业变态辅助的修炼之路 已完成27课… 【第7款】大杀特杀分析来源与CALL吸血鬼课程 (56课已完成)
【精品第16款】中控台多线程多开自动化商业辅助课程 已完成66课… 【第8款】完全零基础网页辅助课程(40课已完成)
【全新精品第17款】检测原理与过游戏内存检测技术课程 已发布9课… 【第9款】自动登录与操控LUA技术课程 (共46+8_x64下更新课已完成)…
【全新精品第18款】手游全自动化任务脚本辅助课程 已发布25课…… 【第10款】网页辅助封包脱机进阶课程 已完成30课…
【全新精品第19款】D3D方框骨骼透视与自瞄辅助课程进阶篇 已发布34课…… 【第11款】VC++ Lua脚本辅助课程 已完成112课…
【全新精品第20款】 X64模拟器吃鸡游戏方框透视自瞄辅助课程 发布中... 【第12款】网游脱机封包智辅课程 已完成35课…
查看: 1601|回复: 0

VC多线程下对内存操作优化

[复制链接]

17

主题

8

回帖

581

积分

编程入门

Rank: 1

魔鬼币
581
发表于 2015-11-17 09:52:44 | 显示全部楼层 |阅读模式
许多程序员发现用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毫秒,是前者的上千倍。
    图1的性能图显示了过度的线程切换,而图2显示了同一个进程在同样的环境下,在使用了下面提供的解决方法后的情况。图1的情况下,系统每秒钟要进行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程序检查这个版本号。要改变所要运行的程序的堆函数的选取,在命令行下键入以下命令:
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。该程序接收三个参数,第一个参数表示线程数,第二个参数是所申请的内存的最大值,第三个参数每个线程申请内存的次数。
测试代码如下:

  1. #define WIN32_LEAN_AND_MEAN
  2. #include <windows.h>
  3. #include <process.h>
  4. #include <stdio.h>
  5. #include <stdlib.h>
  6. /* compile with cl /MT heaptest.c */
  7. /* to switch to the system heap issue the following command
  8.    before starting heaptest from the same command line
  9.    set __MSVCRT_HEAP_SELECT=__GLOBAL_HEAP_SELECTED,1 */
  10. /* structure transfers variables to the worker threads */
  11. typedef struct tData
  12. {
  13.     int maximumLength;
  14.     int allocCount;
  15. } threadData;
  16. void printUsage(char** argv)
  17. {
  18.     fprintf(stderr,"Wrong number of parameters./nUsage:/n");
  19.     fprintf(stderr,"%s threadCount maxAllocLength allocCount/n/n",
  20.         argv[0]);
  21.     exit(1);
  22. }
  23. unsigned __stdcall workerThread(void* myThreadData)
  24. {
  25.     int count;
  26.     threadData* myData;
  27.     char* dummy;
  28.     srand(GetTickCount()*GetCurrentThreadId());
  29.     myData=(threadData*)myThreadData;
  30.     /* now let us do the real work */
  31.     for(count=0;count<myData->allocCount;count++)
  32.     {
  33.         dummy=(char*)malloc((rand()%myData->maximumLength)+1);
  34.         free(dummy);
  35.     }
  36.     _endthreadex(0);
  37.     /* to satisfy compiler */
  38.     return 0;
  39. }
  40. int main(int argc,char** argv)
  41. {
  42.     int threadCount;
  43.     int count;
  44.     threadData actData;
  45.     HANDLE* threadHandles;
  46.     DWORD startTime;
  47.     DWORD stopTime;
  48.     DWORD retValue;
  49.     unsigned dummy;
  50.     /* check parameters */
  51.     if(argc<4 || argc>4)
  52.         printUsage(argv);
  53.     /* get parameters for this run */
  54.     threadCount=atoi(argv[1]);
  55.     if(threadCount>64)
  56.         threadCount=64;
  57.     actData.maximumLength=atoi(argv[2])-1;
  58.     actData.allocCount=atoi(argv[3]);
  59.     threadHandles=(HANDLE*)malloc(threadCount*sizeof(HANDLE));
  60.     printf("Test run with %d simultaneous threads:/n",threadCount);
  61.     startTime=GetTickCount();
  62.     for(count=0;count<threadCount;count++)
  63.     {
  64.         threadHandles[count]=(HANDLE)_beginthreadex(0,0,
  65.             &workerThread, (void*)&actData,0,&dummy);
  66.         if(threadHandles[count]==(HANDLE)-1)
  67.         {
  68.             fprintf(stderr,"Error starting worker threads./n");
  69.             exit(2);
  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.     /* 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程序,打开一个命令行窗口,键入以下命令:
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++程序,问题通常发生在当多个线程进行频繁的内存操作活动时。如上文所述,只要进行很少的一些设置,就可能极大地提高多线程程序在多处理器下的性能。这种方法即不需要源程序,也不需要重新编译可执行文件,而最大的好处是用这种方法得到的性能的提高是不用支付任何费用的=
您需要登录后才可以回帖 登录 | 点击注册

本版积分规则

魔鬼作坊|易语言教程|易语言源码|易语言论坛|易语言视频教程| 论坛导航|免责申明|手机版||网站地图
拒绝任何人以任何形式在本论坛发表与中华人民共和国法律相抵触的言论,本站内容均为会员发表,并不代表魔鬼作坊立场!
任何人不得以任何方式翻录、盗版或出售本站视频,一经发现我们将追究其相关责任!
我们一直在努力成为最好的编程论坛!
Copyright© 2010-2019 All Right Reserved.
快速回复 返回顶部 返回列表