把 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\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 的多線程等知識又多了一點了解。感覺技術還是需要沉下心來才能學得好。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM