這篇文章主要是想談談在以CPU為中心的計算體系結構中影響程序性能的主要因素和性能的分析方法以及多線程對程序性能的影響,讀這篇文章首先要具備一定的體系結構和操作系統基礎,特別是進程調度,建議看《Operation System Concept》(中文《操作系統概論》)。
先定義一下程序的性能,就是在單位時間內能執行的任務數或者執行某個任務需要的時間。顯然,在更短的時間內執行更多的任務性能就越高。
CPU和IO操作
言歸正傳,先看一個經典的入門的C程序Hello World!
int main(int argc, char * args[])
{
int m = 0;
for (int i = 0;i < 10;i ++)
{
m = m+i;
}
printf("Hello World! 1+2+…+10=%d\n", m);
return 0;
}
|
這個不算原始的經典的Hello World,比那個Hello World稍微復雜了點,加了一個循環,用來計算1+2+3…+10的值。
如果有了操作系統進程調度的基礎,可以知道這個程序分成兩段執行,第一段是計算1+2+…+10的值,主要在CPU(中央處理單元)中進行,C代碼:
int m = 0;
for (int i = 0;i < 10;i ++)
{
m = m+i;
}
|
第二段是將計算結果輸出到控制臺的這段,將一串文本通過顯卡驅動,傳送到顯示器上顯示,主要在顯卡上進行,C代碼:
printf("Hello World! 1+2+…+10=%d\n", m);
|
整個程序順序執行,所以CPU先計算完成得到1+2+…+10的值后,將這個值轉換成一串字符串,然后將字符串發送給顯示器,等待顯示器顯示完成后,整個程序結束,如果將CPU執行表示為藍色,將顯示器執行表示為紅色,那么程序執行流程如下:

500)this.width=500;" border="0">
圖1
假設CPU中計算1+2+…+10和將這個值變成字符串花費了11ns(納秒),而顯卡將字符串顯示到顯示器上花費了7ns,那么整個程序運行花費了18ns。
Hello World是最簡單的程序,也是所有其他程序的基礎,在以CPU為中心的計算機結構中,內存負責程序的存儲,CPU負責程序的運算和流程控制,其他元件被看成跟上面Hello World中顯卡類似的外圍設備,也被稱作IO設備,所以任何程序都可以看作是一系列CPU操作和一系列IO操作的符合體,如下圖所示:

500)this.width=500;" border="0" width="500">
圖2
所以影響程序性能的主要因素有兩個方面:一是CPU操作的快慢,二是IO操作的快慢。
所以程序性能分析的主要方法就是正確區分哪些是CPU操作,哪些是IO操作。
CPU操作通常有這些:
賦值和計算,如:m = i*j;
流程控制,如:while(true) { i ++;}
IO操作通常有這些:
磁盤文件操作。
網絡操作。
鍵盤和鼠標操作。
顯卡操作,如在屏幕上繪圖,顯示文本等。
USB操作。
串口操作。
紅外線操作。
磁帶機操作。
通常除CPU和內存外的其他設備都可以看成IO操作,內存之所以不看作IO設備,是因為內存訪問相對IO而言,通常要快幾個數量級,所以像char * buff = new char[100];這樣的操作通常也看作CPU操作。
通過分析劃分出程序的CPU操作和IO操作程序段后,可以有針對性的進行優化。
對于CPU操作,常用的提升性能的方法是優化計算和流程控制代碼,如相乘計算 m = i * 8,可以使用 m = i << 3,因為位操作比乘法操作速度快,通常在某種語言中都會講到程序的優化,就屬于優化CPU操作速度。
對于IO操作,如果IO操作過于頻繁而成為系統瓶頸,可以清除一些不必要的IO操作,也可以更換速度更快的IO設備來提高速度,如把硬盤從5400轉提升到7200轉。
多線程
下面看看多線程對程序性能的影響,什么時候該使用多線程,什么時候使用多線程達不到預期的效果。
多線程是程序里面有像上面那樣的多個執行流程,這些執行流程獨立或者聯合起來完成某些任務。
先看看計算機只有一個CPU,一個IO設備,程序有兩個線程,兩個線程執行同樣的代碼,可以畫出執行流程:

500)this.width=500;" border="0" width="500">
圖3
線程1按正常的執行流程執行,線程2雖然跟線程1執行同樣的代碼,卻出現很多不連續的片段,比如2.2à2.3和2.4à2.5,這是因為只有一個CPU,所以CPU在進行線程1的CPU操作時,不能同時進行線程2的CPU操作,也就是2.4和2.5本來是跟線程1的1.3代碼一樣,但是卻被CPU分兩次執行,因為CPU正在執行1.7。2.2和2.3也是同樣的道理,因為IO設備要執行1.4的代碼,所以2.2和2.3被打斷。但是兩個線程的CPU操作和IO操作在時間上可以重疊,因為他們是不同的設備。
也就是在時間上,CPU和IO設備只能同時做一件事情,CPU和IO設備可以各自做自己的事情。
考察一種極端的情況,假設某個程序沒有IO操作,只有CPU操作,那么流程圖變成:

500)this.width=500;" border="0" width="500">
圖4
線程1將占用所有的CPU時間,線程2將一直等待直到線程1完成,因為線程1完成任務后,依然可以再次執行任務,所以這時使用線程1完成任務和使用線程2完成任務沒有區別,也就是線程2的存在并不會讓程序多完成一些任務,所以線程2的存在,并不能提升程序性能。
所以,如果一個程序只有CPU操作,那么多線程并不能提升程序性能。
同理,如果一個程序只有IO操作,那么多線程并不能提升程序性能。
但是多線程在現實中確實有提高程序性能的時候,那是因為實際的程序像圖3那樣,有CPU操作和IO操作組成,CPU操作和IO操作在時間上可以重疊,所以,同一時間內,程序可以做更多的事情。
如果一個線程中CPU操作時間為M,IO操作時間為N,那么在單位時間內,平均有M/(M+N)在處理CPU操作,有N/(M+N)的時間CPU空閑,如果要讓CPU充分利用,那么可以增加(N/(M+N))/(M/(M+N))=N/M個線程來填補CPU操作的空白,這樣CPU能100%被利用,如果線程再增加,CPU沒有空閑,幾乎不會增加程序性能。
所以,讓CPU 100%利用的線程最大數為1+N/M。
同你,讓IO設備100%利用的線程最大數為1+M/N。
這兩個公式只是一個度量式,不是一個計算式,因為隨著線程數的增加,CPU操作時間和IO操作時間將會隨著變化,M和N不再固定。
看兩種常用的程序,服務器程序和用戶交互程序。
服務器程序通常提供某種網絡服務,如WEB服務器,這種程序要求能最大化的利用CPU和IO,在單位時間內處理盡可能多的任務,所以應該使用盡可能時CPU和IO都滿符合工作,多線程數可以取1+N/M和1+M/N中較小的值,如果觀察服務器的CPU和IO使用率,會發現他們常常接近90%。
用戶交互程序通常根據用戶的某些輸入進行相應的操作,操作完成再次等待用戶輸入,如Microsoft Word,要求對用戶的輸入能及時反應,所以操作線程的CPU操作和IO操作應該有一定的空閑,使得用戶輸入線程能隨時獲取CPU來響應用戶的輸入,使用Microsoft Windows時,打開任務管理器,可以發現CPU使用率常常很低,如1%~20%。
IO復用
從上面的分析可以看出,多線程提升程序性能,主要得益于讓CPU和IO設備能并行操作,另一種讓CPU和IO設備并行操作的方法是IO復用,基本的思想是需要進行IO操作時,只是發送一個IO操作請求給IO設備而不必等待IO完成,CPU操作可以繼續進行,IO操作完成后通過某種方法如事件通知程序,然后程序做相應的處理,流程如下:

500)this.width=500;" border="0" width="500">
圖5
以前需要18ns執行的程序,現在只需要11ns就可以完成,性能提升。
常用的文件異步操作、網絡異步操作都屬于IO復用。
使用IO復用后,程序通常只需要一個線程就可以完成所有的功能,減少操作系統線程間切換的開銷,并且不需要線程間同步,但是IO復用需要使用特定的方法監視IO狀態,開發相對比較復雜。
Window 2000的IOCP(IO Complete Port)就是基于IO復用的思想。
總結
雖然上面的結論是在一個CPU并且沒有考慮操作系統的進程調度和內存管理等因素的影響的前提下得出的,但是在以CPU為中心的計算機體系結構中,CPU操作和IO操作的劃分確實普遍適用的,進程調度和內存管理本身也可以看成是CPU操作和IO操作復合的程序,對于多CPU的系統和多IO設備的系統,分析的基礎是所有這些設備能并行操作,所以上面得出的結論是普遍適用的。
在分析過程中,對很多結論使用了粗體字,是為了醒目,不要死記硬背,要記住的是基本原理和分析方法,這樣才能放之四海而皆準。
轉自:http://blog.chinaunix.net/u1/52224/showart_417513.html