把 CPU “玩”起来
前言
从开始学习编程之后,就渐渐痴迷于技术,平时遇到购书满减活动时就忍不住买一堆书。前两天闲着无聊,翻开了去年买的《编程之美》,目录里的“让 CPU 占用率听你指挥”吸引力我的眼球。这一年来捣鼓数据挖掘和机器学习,总会关注代码运行效率,偶尔会思考如何提高 CPU、GPU 的利用率。于是马上翻开了这一节。
让 CPU 利用率听你指挥
翻开后是一道编程题(3星,需要查阅一些资料,在60分钟内完成)
写一个程序,让用户来决定 Windows 任务管理器(Task Manager)的 CPU 占用率。程序设计的越精简越好,语言不限。例如,可实现下面三种情况:
- CPU 和占用率固定在50%,为一条直线
- CPU 的占用率为一条直线,具体占用率由命令行参数决定(参数范围 1~100)
- CPU 的占用率状态是一条正弦曲线
怎么实现呢
稍微想了想,如果想让 CPU 跑满,写一个死循环就好了,让 CPU 一直处于运行状态,那 50% 的利用率要怎么实现呢?一半时间运行一半时间休息,emmmmm。。休息。。突然想到了多线程里常用到的 sleep。接着往下看,确实是使用 sleep。
那就写写代码吧
while True: for i in range(7200000): pass time.sleep(0.01)
这里稍微解释下为什么是 7200000,以及为什么睡眠 0.01s(10ms)。
笔记本的 CPU 是 1.8Ghz,每秒运行次数大概为 1.8 * 10^9 次,假设 CPU 每个时钟周期可以执行两条代码,然后对于一段 for 循环代码,转换成汇编如下
next: mov eax, dword ptr[i] ; i放入寄存器 add eax, 1 ; 寄存器+1 mov dword ptr [i], eax ; 寄存器赋回i cmp eax, dword ptr [i] ; 比较i和n j1 next ; i小于n时重复循环
即5条代码,所以,1S 内循环次数为 1.8 * 10^9 * 2 / 5 = 720000000。而睡眠 10ms 是因为接近 Windows 的调度时间片。
运行了一下,只是稳定在 30% 左右,暂时先不调整循环次数,接着往后看。
可以看出来,这样设置利用率很麻烦,那有没有什么方法可以快点设置呢?
重新看看上面这段代码, 7200000 次循环花费的时间大约为 10ms,那意思就是 CPU 运行 10ms 然后再休息 10ms,再运行 10ms 再休息 10ms,接着运行 10ms 然后再休息 10ms ······ 想必肯定看出来什么了吧,我们只需要设置 CPU 运行多少时间就好了!于是可以写出下面代码
busyTime = 0.01 while True: startTime = time.clock() while((time.clock() - startTime) <= busyTime): pass time.sleep(busyTime)
运行一下,跟刚刚差不太多,稳定在 30% 左右
正弦函数
这时候,我们也可以很容易就写出跑成正弦函数图像的代码了,不断改变运行与空闲的时间比就好了。
import time import mathimport affinity from multiprocessing import Process, cpu_count def exec_fun(): SAMPLING_COUNT = 200 # 抽样点数量 PI = math.pi # pi TOTAL_AMPLITUDE = 300 # 每个抽样点对应时间片 busySpan = [] amplitude = TOTAL_AMPLITUDE / 2 radianIncrement = 2.0 / SAMPLING_COUNT radian = 0.0 for i in range(SAMPLING_COUNT): busySpan.append((amplitude + math.sin(PI * radian) * amplitude) / 1000.0) radian += radianIncrement # print(busySpan[i], TOTAL_AMPLITUDE - busySpan[i]) j = 0 while True: startTime = time.clock() # print(startTime) while ((time.clock() - startTime) <= busySpan[j]): pass # print('sleep') time.sleep(0.3 - busySpan[j]) j = (j + 1) % SAMPLING_COUNT exec_fun()
运行一下。emmmmmmmmmmmm。。。。等一下,不对啊,怎么不是正弦函数形状呢?
这跟说好的好像不太一样啊。是不是因为用的是 python,跑的本来就慢的原因?那试试 C++ 吧
#include<stdlib.h> #include<Windows.h> #include<math.h> const int SAMPLING_COUNT = 150; const double PI = 3.1415926535; const int TOTAL_AMPLITUDE = 300; int main() { DWORD busySpan[SAMPLING_COUNT]; int amplitude = TOTAL_AMPLITUDE / 2; double radian = 0.0; double radianIncrement = 2.0 / (double)SAMPLING_COUNT; for (int i = 0; i < SAMPLING_COUNT; i++) { busySpan[i] = (DWORD)(amplitude + sin(radian * PI) * amplitude); radian += radianIncrement; printf("%d\t%d\n", busySpan[i], TOTAL_AMPLITUDE - busySpan[i]); } DWORD startTime = 0; for (int j = 0;; j = (j + 1) % SAMPLING_COUNT) { startTime = GetTickCount(); while ((GetTickCount() - startTime) <= busySpan[j]); Sleep(TOTAL_AMPLITUDE - busySpan[j]); } return 0; }
再运行一下,它怎么还是这样???
于是乎捣鼓了 2 个小时。。。
……
……
……
后来仔细想了想,CPU 是 4 核 8 处理器的,不会是任务分摊到了几个处理器上了吧?于是查了查如何把当前进程放在一个处理器上执行。
if __name__ == "__main__": p = Process(target=exec_fun) p.start() pid = p.pid print(affinity.get_process_affinity_mask(pid)) affinity.set_process_affinity_mask(pid, 1)
运行一下,好的,它成了!!!
顺便解决下上面C++的代码,在 main() 函数最开始加入下面代码
SetThreadAffinityMask(GetCurrentThread(), 1);
小节
好久没有这样子捣鼓过东西了,想想上次做操作系统课设的时候,要获取系统的信息,当时只是为了完成任务就没有去深究一些东西,这次捣鼓了 CPU 的利用率控制之后,对进程、CPU 以及 python 的多线程等知识又多了一点了解。感觉技术还是需要沉下心来才能学得好。