為什麼5%的CPU佔用會造成這麼大的性能損失?
為簡化問題給出以下例子程序
test1.cpp
#include "mkl.h"
const int size=1024;int main()
{
float* A=new float[size*size];
float* B=new float[size*size];
float* C=new float[size*size];
for(int i=0;i&<10000;i++) { cblas_sgemm(CblasRowMajor,CblasNoTrans,CblasNoTrans,size,size,size, 1.0f,A,size,B,size,1.0f,C,size); } return 0; }編譯命令:icpc test1.cpp -mkl -o test
單獨運行此程序使用time計時耗時20.494s。如果同時運行一個Python程序
i=0
while 1:
i=i+1
則耗時36.651s。
明明干擾的Python程序只佔用了5%的cpu,卻使MKL的程序慢了近一倍。這是為什麼呢?有什麼方法可以解決這個問題
你這是典型的oversubscription,做數值計算會導致很大的性能損失。
一般的oversubscription,依我愚見,可能的性能損失原因有下面幾種:1. mkl裡面這個地方可能使用的是static schedule。對於blas,這是很合理的,因為每個線程的計算量都是固定的,很容易負載平衡,不需要dynamic,static具有最好的效率。這種情況下,20個線程每個線程的計算任務是一樣多的,如果有一些線程完成了另一些還沒有,那些已經完成的線程只能等。如果你用一個python佔用了一部分cpu,擠佔了一部分線程的資源。一部分線程進度比其他的慢,其他的完成後只能等。舉例,最壞的情況下,python進程一直擠占的線程0的資源,線程0的速度變成原來的一半,那整個程序耗時就增加一倍了。2. oversubscription會增加cache miss的可能性 (另外一個答案說的沒錯,針對具體這個python代碼,這個因素可能影響不大。但是我前面說了,我這三點都是針對「一般的oversubscription」說的。針對一般化的代碼的oversubscription,因為mkl的內部矩陣分塊等等應該都是根據緩存大小優化了的,有別的進程佔用太多緩存的話,導致mkl可用緩存少於預期,是很可能導致嚴重性能損失的。)3. 頻繁的contex switch佔用資源。順便問一句,你這個是雙路20核還是單路10核20線程?如果是後者並且此機器主要用作數值計算,建議bios裡面關閉超線程。10核20線程跑20個軟體線程的blas,一樣是oversubscription。要解決的話,很簡單。mkl不要使用全部的核心。比如給他分配19個核心,留一個給python即可。
設置OMP_NUM_THREADS環境變數可以實現。另外如果是雙路20核,一定要綁定線程,否則numa問題會導致性能損失,單路的情況好點。MKL 某些版本在某些平台上就是有 over subscription 的 bug。質量很低的一個庫,雖然確實有優化的獨門。
我在一台雙路12核的機器上跑了下題主的程序,性能差不多是沒有干擾67s左右,加一個死循環進程干擾是116s左右。MKL利用OpenMP做並行,只佔用物理核心,不會過多地發射線程,HT沒影響。試了下MKL_NUM_THREADS縮掉一個核心沒用,該慢還是慢,題主用的機器有20個核,負載均衡影響沒那麼大。死循環不訪存,和cache miss應該沒啥太大的關係。這個問題和線程創建、銷毀的時間有關,for循環10000次才跑了30s,也就是說每次函數執行大概只有3ms的時間,這樣的話大部分時間都在創建、銷毀進程、分配任務,而不是計算。有相當一部分時間是單線程狀態下的,所以按照核心數算5%的性能損失並不合適。另外,在這種工作狀態下,操作系統需要搞定各種各樣的鎖、同步等等,如果有個死循環在那不停地oversubscribe/context switch,就很容易被干擾,性能下降。為了證明這點,我把題主程序的size擴大了10倍,相當於計算規模擴大100倍。然後循環砍到5次,結果如下:情況1 無干擾 12線程
real 0m28.861s
user 5m30.907s
sys 0m0.906s
情況2 有干擾 12線程real 0m39.559s
user 6m20.002s
sys 0m1.165s
情況3 有干擾 11線程real 0m31.802s
user 5m34.870s
sys 0m0.896s
差不多這個意思,懶得反覆測試算平均值了。情況2的39秒到情況3的31秒避免了靜態負載分配不均,性能稍微好點,基本符合預期。
另外。。。題主你new個數組不delete,傳給計算函數還不初始化,雖然應該沒啥問題但是我很難受啊!
#include "mkl.h"
const int size=10240;
int main()
{
float* A=new float[size*size];
float* B=new float[size*size];
float* C=new float[size*size];
for(int i = 0; i &< size * size; ++i){ A[i] = i; B[i] = i; } for(int i=0;i&<5;i++) { cblas_sgemm(CblasRowMajor,CblasNoTrans,CblasNoTrans,size,size,size, 1.0f,A,size,B,size,1.0f,C,size); } delete [] A; delete [] B; delete [] C; return 0; }
我不了解mkl,但是有寫計算框架的經驗。有時候我們會為了榨乾性能而把thread綁定在核心上以減少context switch. 假設mkl看到你有20核,決定每個核心跑5%的工作量,那麼必然有一個核心會和python搶那5%的資源。。。
計算結束時間由最慢的部分完成的時間決定。
https://software.intel.com/zh-cn/blogs/2012/03/20/intel-mkl/
這個文檔里有設置線程數量的方法。你調成18或19試試。應該是cpu時間片輪轉的原因吧,每個進程不能保證自己是最優先順序。cpu的執行順序可能是:「python程序-自己寫的程序-python程序-自己寫的程序.....」,這樣循環執行,極端情況也可能是:「python程序-python程序-python程序-...-自己寫的程序-python程序-python程序-python程序-...-自己寫的程序.....」 ,這樣循環執行。如果是這種情況那麼就會很嚴重的影響你所要執行的程序的速度。而cpu的佔用率只單純計算其裝載率,和那個進程能夠盡量多的快的搶到cpu的時間片無關。
會不會是context switch造成的額外開銷並沒有被統計到process time里?
推薦閱讀:
※為什麼工控還在用c?
※C++中左值、右值與寄存器的關係是怎樣的?
※LOL盒子這類的輔助工具一般都是用什麼開發的?
※一個典型的遊戲循環是怎樣的流程?
※國內/外的編程圈子有什麼不一樣?