把 CPU “玩”起来

Posted csu-lmw

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了把 CPU “玩”起来相关的知识,希望对你有一定的参考价值。

前言

从开始学习编程之后,就渐渐痴迷于技术,平时遇到购书满减活动时就忍不住买一堆书。前两天闲着无聊,翻开了去年买的《编程之美》,目录里的“让 CPU 占用率听你指挥”吸引力我的眼球。这一年来捣鼓数据挖掘和机器学习,总会关注代码运行效率,偶尔会思考如何提高 CPU、GPU 的利用率。于是马上翻开了这一节。

让 CPU 利用率听你指挥

翻开后是一道编程题(3星,需要查阅一些资料,在60分钟内完成)

写一个程序,让用户来决定 Windows 任务管理器(Task Manager)的 CPU 占用率。程序设计的越精简越好,语言不限。例如,可实现下面三种情况:

  1. CPU 和占用率固定在50%,为一条直线
  2. CPU 的占用率为一条直线,具体占用率由命令行参数决定(参数范围 1~100)
  3. 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	%d
", 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 的多线程等知识又多了一点了解。感觉技术还是需要沉下心来才能学得好。

以上是关于把 CPU “玩”起来的主要内容,如果未能解决你的问题,请参考以下文章

CPU 和内存虚拟化原理 - 每天5分钟玩转 OpenStack

Java代码是如何被CPU狂飙起来的?

火焰图--记一次cpu降温过程

分享几个实用的代码片段(附代码例子)

分享几个实用的代码片段(附代码例子)

没GPU也能玩梵高作画:Ubuntu tensorflow CPU版