《编程之美》之CPU曲线
1.CPU相关:(摘自MSDN——Processor Groups)
Systems with more than one physical processor or systems with physical processors that have multiple
cores provide the operating system with multiple logical processors. A logical processor is
one logical computing engine from the perspective of the operating system, application or driver. A core is
one processor unit, which can consist of one or more logical processors. A physical processor can
consist of one or more cores. A physical processor is the same as a processor package, a socket, or a CPU.
从操作系统(或者是应用程序、驱动)的角度看,一个逻辑处理器就是一个逻辑计算引擎。一个内核就是一个处理单元,它可以由一个或多个逻辑处理器组成。一个物理处理器可以由一个或多个内核组成。
以我的电脑为例,CPU为i3-2330M,2.2GHz,2个内核,4个逻辑处理器(也叫做双核四线程)。2个内核拥有2*2个逻辑处理器,得益于Intel的超线程技术(HT),该技术可实现在一个实体处理器中提供两个逻辑线程,也就是说一个处理器可模拟出两个处理器。在Windows任务管理器里就可以体现出来。可以看出,有4个小窗口。
注意:若其中一个逻辑处理器满负荷运转,则CPU的使用率为25%,若控制一个逻辑处理器的使用率为50%,则CPU的使用率为12.5%。
2.直线CPU
要控制CPU的使用率,就必须了解一个函数——Sleep(),该函数的作用是让程序让出CPU时间,除了Sleep,其他的操作都会占用CPU时间。
①根据CPU速度估算
i3-2330M的速度达到2.2GHz,一个for循环for(i=0;i<n;i++)转换成汇编语句:
loop:
mov dx i;将i置入dx寄存器
inc dx;将dx寄存器加1
mov i dx;将dx中的值赋回i
cmp i n;比较i和n
jl loop;i小于n时则重复循环
共5句汇编,现代CPU每个时钟周期可执行两条以上的指令,那么每秒可执行for循环的次数为2.2G*2/5次,执行代码如下:
void LineCPU1() { SetThreadAffinityMask(GetCurrentThread(), 8); long i; while(1) { for(i=0;i<8800000;i++); Sleep(10); } }这里选用10ms是因为10ms比较接近Windows调度时间片,若该值太大,会使得曲线不平滑,一下很高,一下很低;若该值太小,会使得线程被频繁地唤醒和挂起,无形中又增加了内核时间不确定性的影响。
从执行效果可看到,该程序(test2.exe)CPU使用率为18%,大于我们希望的12.5%,我将n改为4400000,使用率会低至14%,但还是不是12.5%。可见这种方法比较粗糙,且只适用于特定频率的CPU,具有CPU差异性。(笔记本出于省电的考虑,CPU的时钟频率可能会变化)
②利用GetTickCount()消除CPU差异性
GetTickCount()函数可以得到“系统启动到现在”所经历的毫秒数,最多可统计到49.7天,执行效果:平均CPU使用率基本可维持在12.5上下
void LineCPU2() { SetThreadAffinityMask(GetCurrentThread(), 8); int busyTime = 100; int idleTime = busyTime; long startTime = 0; while(1) { startTime = GetTickCount(); while((GetTickCount() - startTime) <= busyTime); Sleep(idleTime); } }
以上两种方法都是假设只有该进程运行在逻辑处理器上,但事实上系统运行时会有其他进程也在运行,若其他进程占用了5%,那么我们的程序就应该占用45%。那么如何解决这个问题呢。
③PerformanceCounter类
PerformanceCounter是.Net类库中的一个类(通过它可以很方便地获取当前的各种性能数据,包括CPU使用率),所以需要用C#来写,需要命名空间using System.Diagnostics,该程序没怎么具体去试
PerformanceCounter pc = new PerformanceCounter("Processor", "% Processor Time", "_Total"); while (true) { if (pc.NextValue() > 18) System.Threading.Thread.Sleep(10); }
3.正弦CPU
有了以上的铺垫,那么正弦的CPU曲线就很容易啦
void SinCPU() { SetThreadAffinityMask(GetCurrentThread(), 8); const double SPLIT = 0.01; const int COUNT =200; const double PI = 3.14159265; const int INTERVAL = 300; int busySpan[COUNT]; int idleSpan[COUNT]; int half = INTERVAL/2; double rad =0.0; for(int i=0;i<COUNT;i++) { busySpan[i] = (int)(half+(sin(PI*rad)*half/2)); idleSpan[i] = INTERVAL - busySpan[i]; rad += SPLIT; } long startTime = 0; int j = 0; while(1) { j = j%COUNT; startTime = GetTickCount(); while((GetTickCount() - startTime)<=busySpan[j]); Sleep(idleSpan[j]); j++; } }
4.指定某个处理器上运行
SetThreadAffinityMask(hThread, dwThreadAffinityMask)函数的作用是指定该线程在某一个逻辑处理器上运行,每个处理器代表1bit,即1代表第1个处理器,2代表第2个处理器,4代表第3个处理器,8代表第4个处理器,从下面两张图可看出区别,一个是完整的正弦,一个是有缺口的正弦
当第一个正弦有缺口时,第二、第三个处理器有出现相应的峰值,可见当一个程序在某个处理器中接近满负荷时,会被分配到另外几个处理器中,以减小处理器的压力
注:SetThreadAffinityMask(hThread, dwThreadAffinityMask)、Sleep()、GetTickCount()函数需要"Windows.h"头文件,数学函数sin()需要"math.h"头文件,CPU的频率会随着供电和负载情况的变化而变化。