1. 程式人生 > >C/C++程式CPU問題分析

C/C++程式CPU問題分析

轉載地址:http://www.10tiao.com/html/473/201606/2651473094/1.html

  程式的CPU問題是另外一類典型的程式效能問題,很多開發人員都受到過程式CPU佔用過高的困擾。本次我們收集了14個CPU類的問題,和大家一起分析下這些問題的種類和原因。另外,對於C/C++程式而言,目前已經有了很多CPU問題定位的工具,本文也會進行比較分析。

CPU問題分析

  程式CPU類問題的主要現象是:程式佔用的CPU過高,比程序升級前有很大的升高。導致程式CPU佔用過高的主要原因是程式設計不合理,絕大部分的CPU問題都是程式設計的問題。因此,提高程式的設計質量是避免CPU問題的主要手段。

1.1 大量低效操作引起的問題

在程式設計中,有些程式的寫法是比較低效的,沒有經驗的同學很容易使用一些低效的函式或方法,這就是我們常說的“坑”。我們蒐集到了一些“坑”,跟大家分享下。

memset是一個很常見的效能坑。如果在程式中使用的memset過多,會導致程式的CPU消耗很大。memset使用過多,往往在不經意間就讓程式下降了一大截。關於memset函式,一種常見的誤用是在迴圈中對較大的資料結構進行memset。在這個例子中,一個query中memset 1M的內容,在整體1500qps的情況下,每秒進行重置的記憶體達到1.5G,導致程式的CPU IDLE答覆下降。

上面這個例子,是memset一種比較明顯的問題,通過程式碼review等方式是比較容易發現的。在一些情況下,memset操作是在隱式發生的,問題的排查難度也隨之加大。

#程式碼片段1
char buffer[1024] = {0};
  • 1
  • 2

程式碼片段1中的簡簡單單的一行程式碼,其實在實際的執行過程中是會呼叫memset的。這個就是一個坑:在棧記憶體中申請緩衝區,然後再賦值,會隱式的呼叫memset,將記憶體初始化為0。這個問題也導致了一個產品線的核心模組效能大幅下降,引起了嚴重的效能問題。

  另外,在使用一些系統函式或庫函式時,也需要仔細閱讀使用手冊,避免出現大量的無效的記憶體申請、釋放和重置操作。

程式碼片段2
memset(&t_data->preq, 0, sizeof(pusrinfo_req_t));
memset(&t_data->pres, 0, sizeof(pusrinfo_res_t)); odb_renew(t_data->cur_field_dict);
  • 1
  • 2
  • 3
  • 4
  • 5

程式碼片段2中的這段程式碼中,第1、2行中的memset會導致程式的CPU使用過多,但即使是將這兩行的程式碼註釋掉,程式的效能依然沒有明顯的改觀。問題的根源在於程式碼片段2中最後一行程式碼呼叫的odb_renew函式有釋放記憶體和大量的memset操作,導致消耗的CPU很多。如果在程式中呼叫了大量的odb_renew函式,其效能一定不太好。

  strncpy這個字串操作函式是比較耗費效能的,同strncpy函式實現類似功能的函式有snprintf和memcpy+strlen這兩種方式。表1是在一臺測試機上對這三種方式的效能比較。 
這裡寫圖片描述
從表1可以看出,memcpy的效能最好。令人欣喜的是snprintf在大資料下效能漸漸逼近memcpy。稍微看了一下幾個函式的原始碼,memcpy用了page copy和word copy結合,所以效能優化的比較好,而且strlen也是用4位元組做迴圈步長的。strncpy只是簡單地逐位元組拷貝,並且會將目標buffer後面所有的空閒空間全部填為0,這在很多情況下是非常耗費效能的。

  整體上,對於這類問題的主要解決方法是:識別CPU消耗多的函式並且儘量減少這類函式的使用。比如,有些資料結構的memset是沒有必要的,這些資料結構會被下一個query的資料自然填充。又或者採用更高效的初始化的方法。典型的例子是,字串陣列的初始化,只需要將第一個字元設定為0即可。

1.2 容器使用不當引起的問題

  程式設計中,容器的使用是必不可少的。不同型別的容器,其設計的目的是不同的,因此某些方面的效能天然地會比較低。我們在程式設計的時候,要能夠正確的識別容器各種用法的效能,減少低效的使用。 
這裡寫圖片描述
程式碼片段3中的第6行程式碼,將計算列表長度的方法放到了迴圈中,本身list型別求取長度的函式複雜度就是O(n),在這個操作放到迴圈中以後,直接將這段程式碼的複雜度提高到了O(n2),在列表中元素較多的情況下,對程式的效能將產生非常大的影響。程式碼片段3中的例子2是另外一種錯誤用法,演算法複雜度也是O(n2)。 
這裡寫圖片描述

  上面的兩個例子,還有一個典型的特點,就是存在迴圈。對於迴圈程式來說,要儘量避免在迴圈體內進行大量消耗CPU的操作。即使是每次消耗的CPU較少,但是由於存在迴圈,演算法複雜度提升了一個數量級,因此要特別的小心。

1.3 鎖&上下文切換過多引起的問題

&meps; 程式中存在過多的加鎖/解鎖操作,是程式CPU效能惡化的另外一大類原因,其典型的現象是:系統態的CPU過高,甚至超過了使用者態CPU。 
  自旋鎖和互斥鎖一樣,是常見的解決系統資源互斥的方法。與互斥鎖不同,自旋鎖不會引起呼叫者睡眠,如果自旋鎖已經被別的執行單元保持,呼叫者就一直迴圈在那裡看是否該自旋鎖的保持者已經釋放了鎖。一般情況下,自旋鎖鎖定的資源釋放的都比較快,在這種情況下,由於呼叫者不需要睡眠,減少了系統的切換,因此可以提高程式的效能。但隨著程式處理能力、流量、資料大小的變化,自旋鎖有時也會導致程式效能惡化。在我們收集的一個百度知道的案例中,程式訪問cache的時候,通過自旋鎖進行同步的控制。程式剛開始上線時並無問題,但隨著流量的增大,當程式的qps達到1700時,系統態CPU高達73%,自旋鎖引起了嚴重的效能瓶頸。對於這個case,主要的解決方案就是要“去鎖”,減少鎖操作。 
  另外一個例子也是關於加鎖過多的。鳳巢檢索端的一個模組,處理一個請求時,每次最多可以得到4096個詞,程式需要獲取這些詞的資訊,這些資訊大部分是儲存在cache中,如果cache中不存在,則需要重新計算並更新cache。曾經一個存在的問題是,程式的設計邏輯是每次從cache中查詢一個詞的資訊,並且在查詢cache時要進行加鎖/解鎖操作。那麼一次請求,最多要進行4096次操作。對於一個qps達到1000的程式來說,每秒的加鎖操作達到了百萬級,程式的效能嚴重惡化。 
  在作業系統課程中我們學習過,當執行緒需要等待一定的條件時會被作業系統放入的休眠佇列中,直到被喚醒。程式的上下文切換過多,也會導致程式的效能惡化。曾經在在一個模組中出現這樣的現象,機器的系統態CPU出現週期性的增長,現象如圖1所示。 
這裡寫圖片描述 
  經過排查,發現引起這種現象的原因是程式碼片段5中的一行shell程式碼引起的。這行程式碼的作用是將日誌中含有keyword的最後100條日誌找出來,並進行重新寫資料。這行程式碼會被週期性的執行。圖2給出了程式碼執行過程的示意圖。grep寫標準輸出還經過標準C庫這麼一層緩衝,緩衝區大小預設是4K,也就是說grep先呼叫fwrite寫標準C庫緩衝區,寫滿4K以後,標準C庫呼叫write系統呼叫將標準C庫緩衝區刷到核心中的管道緩衝區,然後tail程序呼叫read系統呼叫從核心中的管道緩衝區一次性讀取4K位元組。很明顯,grep寫滿核心中管道緩衝區以後,必須等待tail讀取完成,才能繼續寫,那麼這個時候,它就要被切換出去, 進入一個等待佇列,tail程序被切換進來,讀取4K位元組,然後喚醒grep,tail被切換出去,grep被切換進來……隨著需要grep的檔案越來越大,程序切換的次數也越來越多,系統態的CPU佔用也水漲船高。 
這裡寫圖片描述

1.4 其他問題分析

  還有很多情況,都可能導致程式的CPU消耗過多,比如I/O操作過多。I/O操作過多問題中最常見的一類是程式列印了過多的日誌。曾經在後羿系統就發生這樣的例子,由於程式輸出的日誌從二進位制升級為了字串,整體的I/O量增加了30%,導致程式的吞吐量從3.8萬降低到了2.1萬,幾乎下降一半。還有一個典型的I/O問題是程式中有很多的DEBUG日誌,雖然最終在線上沒有開啟DEBUG日誌列印,但是程式在執行過程中還是會走到DEBUG日誌相關的程式邏輯,只是不進行日誌的輸出。如果在日誌輸出的地方,存在複雜的計算邏輯,那麼程式的效能也下降。程式碼片段6中的程式碼就是一個例子。在這個debug日誌輸出的過程中呼叫了material物件的to_string函式,而這個函式非常的消耗效能。儘管程式最終沒有輸出DEBUG日誌,但是to_string函式還是被呼叫到了,程式的效能依然會受到影響。 
這裡寫圖片描述

Fast JSON是阿里巴巴提供的開源JSON工具,支援對JSON的序列化和反序列化的功能,號稱是最快的JSON解析工具,在百度電影的部分模組中使用了這個工具。Fast JSON的1.2.2版本存在呼叫java.lang.System.getProperty時,多執行緒需要加鎖,會帶來執行緒hang住,引起系統性能降低的問題。這個問題導致了電影的這個模組出現了比較嚴重的線上問題。

CPU問題定位工具比較分析

對於C/C++程式,目前業界使用的比較多的CPU熱點定位工具有:valgrind中元件callgrind,gprof(GNU Profiler),google perf tools元件中的CPU Profiler和Oprofiler。

• callgrind工具(valgrind套件之一):valgrind整體採用虛擬機器的解決方案,將被測程式的指令轉換了valgrind自身的程式碼Ucode,這樣就可以實現對被測程式全面的分析(CPU, MEM)。

• gprof(GNU Profiler)工具 : GNU提供的工具,已經存在了30年左右了。主要通過在函式入口處插入程式碼的方式來統計函式的呼叫關係、次數及CPU使用方式。

• google perf tools(CPU Profile):對程式的呼叫棧進行取樣分析,通過呼叫棧反推出函式的呼叫次數、關係和CPU消耗時間。

• Oprofile :利用cpu硬體提供的效能計數器,通過技術取樣,從程序、函式、程式碼層面分析效能問題。更多的用於分析系統層面個的問題,使用者態cpu只是其中一部分。

在c++ perf tools初體驗這篇文章中,有比較詳細的各類工具的用法和原理說明,有興趣的同學可以深度閱讀。

表2從多個維度對這4種工具進行了比較,綜合比較這些因素後,我還是推薦使用google perf tools套件中的CPU Profiler,這個工具在靈活性、應用性等方面的優勢非常明顯。但就像表格中提到的,這種工具會讓程式一定概率core dump。 
這裡寫圖片描述

總結

本文收集並分析了十幾個C/C++程式CPU效能問題,通過對這些問題的分析,我們發現CPU相關的效能問題,很多都是由於程式設計問題引起的。減少低效的呼叫,充分釋放CPU的能力,是提升程式CPU效能的關鍵。從更大的層面上來看,程式的CPU效能還需要更好的架構設計,充分呼叫各種資源來高效地完成任務。google perf tools套件中的CPU Profiler工具是一個非常優秀的定位CPU熱點的工具,希望大家能夠多用這類工具來優化程式的CPU。