找到性能瓶頸
二八法則適合很多事物:最重要的只占其中一小部分,約20%,其余80%的盡管是多數,卻是次要的。在程序代碼中也是一樣,決定應用性能的就那20%的代碼(甚至更少)。因此優化實踐中,我們將精力集中優化那20%最耗時的代碼上,這那20%的代碼就是程序的性能瓶頸,主要針對這部分代碼進行優化。
常見優化方法:
這部分我就不寫,直接參見《性能調優攻略》,因為我沒有自信能寫出比這更好的。
如果不想這么深入地了解,看看《C++程序常見的性能調優方式》這篇文章也是不錯的。
應用案例
我們以一個應用案例來講解,以至於不會那么乏味難懂。
我們知道能被1和它本身整除的整數叫質數,假設1到任意整數N的和為Sn(Sn=1+2+3+…+n)。現在要求10000到100000之間所有質數和Sn。
可能你會覺得這問題不是So Easy嗎!都不用腦袋想,咣當一下就把代碼寫完了,代碼如下:
#include <iostream> #include <windows.h> // 定義64位整形 typedef __int64 int64_t; // 獲取系統的當前時間,單位微秒(us) int64_t GetSysTimeMicros() { // 從1601年1月1日0:0:0:000到1970年1月1日0:0:0:000的時間(單位100ns) #define EPOCHFILETIME (116444736000000000UL) FILETIME ft; LARGE_INTEGER li; int64_t tt = 0; GetSystemTimeAsFileTime(&ft); li.LowPart = ft.dwLowDateTime; li.HighPart = ft.dwHighDateTime; // 從1970年1月1日0:0:0:000到現在的微秒數(UTC時間) tt = (li.QuadPart - EPOCHFILETIME) / 10; return tt; } // 計算1到n之間所有整數的和 int64_t CalculateSum(int n) { if (n < 0) { return -1; } int64_t sum = 0; for (int i = 0; i < n; i++) { sum += i; } return sum; } // 判斷整數n是否為質數 bool IsPrime(int n) { if (n < 2) { return false; } for (int i = 2; i < n; i++) { if (n %i == 0) { return false; } } return true; }
void main() { int64_t startTime = GetSysTimeMicros(); int count = 0; int64_t sum = 0; for (int i = 10000; i <= 100000; i++) { if (IsPrime(i)) { sum = CalculateSum(i); std::cout << sum << "\t"; count++; if (count % 10 == 0) { std::cout << std::endl; } } } int64_t usedTime = GetSysTimeMicros() - startTime; int second = usedTime / 1000000; int64_t temp = usedTime % 1000000; int millise = temp / 1000; int micros = temp % 1000; std::cout << "執行時間:" << second << "s " << millise << "' " << micros << "''" << std::endl; }
然后運行。
我想這肯定不是你要的結果(太慢了),如果你覺得還滿意,那下面的就可以不用看了。
VS的性能分析工具
性能分析工具的選擇
打開一個“性能分析”的會話:Debug->Start Diagnotic Tools Without Debugging(或按Alt+F2),VS2013在Analysis菜單中。
CPU Usage
檢測CPU的性能,主要用於發現影響CPU瓶頸(消耗大量CPU資源)的代碼。
GPU Usage
檢測GPU的性能,常用於圖形引擎的應用(如DirectX程序),主要用於判斷是CPU還是GPU的瓶頸。
Memory Usage
檢測應用程序的內存,發現內存。
Performance Wizard
性能(監測)向導,綜合檢測程序的性能瓶頸。這個比較常用,下面再逐一說明。
性能(監測)向導
1.指定性能分析方法;
CPU Sampling(CPU采樣):
進行采樣統計,以低開銷水平監視占用大量CPU的應用程序。這個對於計算量大的程序可大大節省監控時間。
Instrumentation(檢測):
完全統計,測量函數調用計數和用時
.NET memory allocation(.NET 內存分配):
跟蹤托管內存分配。這個好像只有托管代碼(如C#)才可用,一般以C++代碼好像不行。
Resource contention data(並發):
檢測等待其他線程的線程,多用於多線程的並發。
2.選擇要檢測的模塊或應用程序;
3.啟動分析程序進行監測。
性能分析報告
程序分析完成之后會生成一個分析報告,這就是我們需要的結果。
視圖類型
有幾個不同的視圖可供我們切換,下面加粗的部分是個人覺得比較方便和常用的視圖。
Summary(概要):整個報告概要說明
Call Tree(調用樹):以樹形表格的方式展開函數之間的關系。
Module(模塊):分析調用的不同的程序模塊,如不同的DLL、lib模塊的耗時
Caller/Callee(調用與被調用):以數值顯示的調用與被調用的關系
Functions(函數統計):以數值顯示的各個函數的執行時間和執行次數統計值
Marks(標記):
Processers(進程):
Function Detials(函數詳情):以圖表的方式形象地顯示:調用函數-當前函數-被調用子函數之間的關系和時間比例。
調用樹
函數詳情
函數統計
專用術語
如果是第一次看這報告,你還不一定能看懂。你需要先了解一些專用術語(你可以對照着Call Tree視圖和Functions視圖去理解):
Num of Calls:(函數)調用次數
Elapsed Inclusive Time:已用非獨占時間
Elapsed Exclusive Time:已用獨占時間
Avg Elapsed Inclusive Time:平均已用非獨占時間
Avg Elapsed Exclusive Time:平均已用獨占時間
Module Name:模塊名稱,一般為可執行文件(.exe)、動態庫(.dll)、靜態庫(.lib)的名稱。
也許看完你還迷糊,只要理解什么是獨占與非獨占你就都明白了。
什么是獨占與非獨占
非獨占樣本數是指的包括了子函數執行時間的總執行時間
獨占樣本數是不包括子函數執行時間的函數體執行時間,函數執行本身花費的時間,不包括子(函數)樹執行的時間。
解決應用案例問題
我們已經大致了解了VS2015性能分析工具的使用方法。現在回歸本質,解決上面提及的應用案例的問題。
1、我們選擇Function Detials視圖,從根函數開始依據百分比最大的項選擇,直到選擇PrintPrimeSum,這時可以看到如下圖:
找出性能瓶頸1
我們可以看到IO占了50%多(49.4%+9.7%)的時間,所以IO是最大的性能瓶頸。其實,有一定編程經驗的人應該都能明白,在控制台輸出信息是很耗時的。我們只是需要結果,不一定非要在控制中全部輸出(這樣還不便查看),我們可以將結果保存到文件,這樣也比輸出到控制台快。
注:上圖所示的時間,應該是非獨占時間的百分比。
知道了瓶頸,就改進行代碼優化吧:
void main() { int64_t startTime = GetSysTimeMicros(); std::ofstream outfile; outfile.open("D:\\Test\\PrimeSum.dat", std::ios::out | std::ios::app); int count = 0; int64_t sum = 0; for (int i = 10000; i <= 100000; i++) { if (IsPrime(i)) { sum = CalculateSum(i); outfile << sum << "\t"; count++; if (count % 10 == 0) { outfile << std::endl; } } } outfile.close(); int64_t usedTime = GetSysTimeMicros() - startTime; int second = usedTime / 1000000; int64_t temp = usedTime % 1000000; int millise = temp / 1000; int micros = temp % 1000; std::cout << "執行時間:" << second << "s " << millise << "' " << micros << "''" << std::endl; }
再次執行,發現時間一下減小到
效果很明顯!
2、但這還不夠,繼續檢查別的問題,對新代碼再次用性能分析工具檢測一下。
找出性能瓶頸2
我們發現IsPrime函數占用了62%的時間,這應該是一個瓶頸,我們能不能對其進行算法的優化?仔細想想,上面求質數的方法其實是最笨的方法,稍微對其進行優化一下:
// 判斷整數n是否為質數 bool IsPrime(int n) { if (n < 2) { return false; } if (n == 2) { return true; } //把2的倍數剔除掉 if (n%2 == 0) { return false; } // 其實不能被小於n的根以下的數整除,就是一個質數 for (int i = 3; i*i <= n; i += 2) { if (n % i == 0) { return false; } } return true; }
再次執行,發現時間一下減小到:
幾乎減了一半的時間。
3、這還是有點慢,再看看還能不能進行優化。對新代碼再次用性能分析工具檢測一下。
CalculateSum函數占了88.5%的時間,這絕對是影響目前程序性能的主要因素。對其進行。仔細想想,求1到N的和其實就是求1、2、3 … N的等差數列的和。優化代碼如下:
// 計算1到n之間所有整數的和 int64_t CalculateSum(int n) { if (n < 0) { return -1; } //(n * (1 + n)) / 2 return ( n * (1 + n) ) >> 1; }
再次執行,發現時間一下減小到:
一秒中之內,基本上可以滿足要求子。
總結
程序性能調優,就是數上面這樣一點點地改進的過程,直到滿足應用的要求。上面只用了一個視圖的一種統計指標(各函數所用時間占總時間的百分比),就解決了問題。對於大型的復雜應用程序,我們可以結果多種視圖的多種統計指標進行綜合判斷,找出程序性能的瓶頸!
代碼:
#include <iostream> #include <windows.h> // 定義64位整形 typedef __int64 int64_t; // 獲取系統的當前時間,單位微秒(us) int64_t GetSysTimeMicros() { // 從1601年1月1日0:0:0:000到1970年1月1日0:0:0:000的時間(單位100ns) #define EPOCHFILETIME (116444736000000000UL) FILETIME ft; LARGE_INTEGER li; int64_t tt = 0; GetSystemTimeAsFileTime(&ft); li.LowPart = ft.dwLowDateTime; li.HighPart = ft.dwHighDateTime; // 從1970年1月1日0:0:0:000到現在的微秒數(UTC時間) tt = (li.QuadPart - EPOCHFILETIME) / 10; return tt; } // 計算1到n之間所有整數的和 int64_t CalculateSum(int n) { if (n < 0) { return -1; } int64_t sum = 0; for (int i = 0; i < n; i++) { sum += i; } return sum; } // 判斷整數n是否為質數 bool IsPrime(int n) { if (n < 2) { return false; } for (int i = 2; i < n; i++) { if (n %i == 0) { return false; } } return true; } void PrintPrimeSum() { int64_t startTime = GetSysTimeMicros(); int count = 0; int64_t sum = 0; for (int i = 10000; i <= 100000; i++) { if (IsPrime(i)) { sum = CalculateSum(i); std::cout << sum << "\t"; count++; if (count % 10 == 0) { std::cout << std::endl; } } } int64_t usedTime = GetSysTimeMicros() - startTime; int second = usedTime / 1000000; int64_t temp = usedTime % 1000000; int millise = temp / 1000; int micros = temp % 1000; std::cout << "執行時間:" << second << "s " << millise << "' " << micros << "''" << std::endl; }