前言
從開始學習編程之后,就漸漸痴迷於技術,平時遇到購書滿減活動時就忍不住買一堆書。前兩天閑着無聊,翻開了去年買的《編程之美》,目錄里的“讓 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 的多線程等知識又多了一點了解。感覺技術還是需要沉下心來才能學得好。
