為什麼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盒子這類的輔助工具一般都是用什麼開發的?
一個典型的遊戲循環是怎樣的流程?
國內/外的編程圈子有什麼不一樣?

TAG:英特爾Intel | CC | 科學計算 | 高性能計算 |