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

2021-02-13 百度智能化測試

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

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

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

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

上面這個例子,是memset一種比較明顯的問題,通過代碼review等方式是比較容易發現的。在一些情況下,memset操作是在隱式發生的,問題的排查難度也隨之加大。代碼片段1中的簡簡單單的一行代碼,其實在實際的運行過程中是會調用memset的。這個就是一個坑:在棧內存中申請緩衝區,然後再賦值,會隱式的調用memset,將內存初始化為0。這個問題也導致了一個產品線的核心模塊性能大幅下降,引起了嚴重的性能問題。


另外,在使用一些系統函數或庫函數時,也需要仔細閱讀使用手冊,避免出現大量的無效的內存申請、釋放和重置操作。代碼片段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,這在很多情況下是非常耗費性能的。


(表1:三種字符串拷貝操作方式的性能比較)

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

程序設計中,容器的使用是必不可少的。不同類型的容器,其設計的目的是不同的,因此某些方面的性能天然地會比較低。我們在程序設計的時候,要能夠正確的識別容器各種用法的性能,減少低效的使用。

代碼片段3中的第6行代碼,將計算列表長度的方法放到了循環中,本身list類型求取長度的函數複雜度就是O(n),在這個操作放到循環中以後,直接將這段代碼的複雜度提高到了O(n2),在列表中元素較多的情況下,對程序的性能將產生非常大的影響。代碼片段3中的例子2是另外一種錯誤用法,算法複雜度也是O(n2)。


與上面的例子類似的一種低效用法是字符串的,代碼片段4給出了示例。for循環中大量的調用strlen,而strlen的算法複雜度為O(n2),放到循環中後複雜度也提升到了O(n2)。


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

程序中存在過多的加鎖/解鎖操作,是程序CPU性能惡化的另外一大類原因,其典型的現象是:系統態的CPU過高,甚至超過了用戶態CPU。

自旋鎖和互斥鎖一樣,是常見的解決系統資源互斥的方法。與互斥鎖不同,自旋鎖不會引起調用者睡眠,如果自旋鎖已經被別的執行單元保持,調用者就一直循環在那裡看是否該自旋鎖的保持者已經釋放了鎖。一般情況下,自旋鎖鎖定的資源釋放的都比較快,在這種情況下,由於調用者不需要睡眠,減少了系統的切換,因此可以提高程序的性能。但隨著程序處理能力、流量、數據大小的變化,自旋鎖有時也會導致程序性能惡化。在我們收集的一個百度知道的案例中,程序訪問cache的時候,通過自旋鎖進行同步的控制。程序剛開始上線時並無問題,但隨著流量的增大,當程序的qps達到1700時,系統態CPU高達73%,自旋鎖引起了嚴重的性能瓶頸。對於這個case,主要的解決方案就是要「去鎖」,減少鎖操作。

另外一個例子也是關於加鎖過多的。鳳巢檢索端的一個模塊,處理一個請求時,每次最多可以得到4096個詞,程序需要獲取這些詞的信息,這些信息大部分是存儲在cache中,如果cache中不存在,則需要重新計算並更新cache。曾經一個存在的問題是,程序的設計邏輯是每次從cache中查詢一個詞的信息,並且在查詢cache時要進行加鎖/解鎖操作。那麼一次請求,最多要進行4096次操作。對於一個qps達到1000的程序來說,每秒的加鎖操作達到了百萬級,程序的性能嚴重惡化。

在作業系統課程中我們學習過,當線程需要等待一定的條件時會被作業系統放入的休眠隊列中,直到被喚醒。程序的上下文切換過多,也會導致程序的性能惡化。曾經在在一個模塊中出現這樣的現象,機器的系統態CPU出現周期性的增長,現象如圖1所示。


(圖1:程序的系統態CPU表現)

經過排查,發現引起這種現象的原因是代碼片段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佔用也水漲船高。



(圖2:代碼執行示意圖)

還有很多情況,都可能導致程序的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住,引起系統性能降低的問題。這個問題導致了電影的這個模塊出現了比較嚴重的線上問題。

對於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。

關注百度質量部訂閱號,回復以下內容立馬查看乾貨哦~

----

 1.回復關鍵詞評測查看評測系列文章
 2.
回復關鍵詞「CI」查看CI系列經驗分享
 3.
回復關鍵詞移動測試查看移動測試系列工具點評

相關焦點

  • python+C、C++混合編程的應用
    我看到的一個很好的Python與c/c++混合編程的應用是NS3(Network Simulator3)一款網絡模擬軟體,它的內部計算引擎需要用高性能,但在用戶建模部分需要靈活易用。NS3的選擇是使用C/C++來模擬核心部件和協議,用python來建模和擴展。這篇文章介紹python和c/c++三種混合編程的方法,並對性能加以分析。
  • C/C++程序內存問題分析
    在《C/C++程序core dump問題分析》[1]中,我們分析了程序出core的問題的類型和幾類原因。
  • 編譯器 | 五款好用的C/C++編譯器(IDE利器)
    這個 IDE 提供超強大的用戶界面開發 C/C++ 程序的接口。codeblocks擴展性能非常強大,也提供了很多工程模板,軟體內置大量的開發插件程序,你可以直接在軟體中進行連接下載,幫助您獲得更高效、穩定、快捷的開發輔助程序,codeblocks新版在項目構建。
  • C/C++程序core dump分析(一)
    那麼程序出core的情況有哪些的?如果程序core了之後,我們應該如何對這類問題進行定位呢?FourExperts小組的同學經過大量的案例收集、篩選和分析,產出了這份分析報告。希望大家從中了解程序出core的常見原因和定位方法。為了給大家一個直觀的認識,我們首先分析一下程序出core的常見原因及分類方法。通過這些分類,我們可以對分core的原因、定位方法有初步的認識。
  • C/C++優勢究竟在哪裡?是什麼讓他們經久不衰?看看這個你就懂了
    相較於C語言,c++誕生於1983年,緊隨c語言的步伐,c++是C語言的超集,大家所知道的C語言是面向過程的,java是面向對象的,那麼C語言為了面向對象,所以誕生出現在大家所熟知的c++,被廣泛視為大規模應用構建軟體。
  • PyTorch的C++ extension寫法
    剛開始我也在思考這個問題,覺得沒有必要。但是後來深入了解了以後發現還是有必要的。舉個慄子,調用始終是使用的是別人的東西,但是擴展則是通過他人的幫助來完成一個屬於自己的東西。pytorch的C++ extension和python的c/c++ extension其實原理差不多,本質上都是為了擴展各自的功能,當然也為了使程序運行更加有效率,差別在於pytorch的C++ extension實施步驟較python的c/c++ extension的要簡化一些。
  • 【藏經閣】C/C++程序性能案例分析-內存問題分析
    ▌3.1 內存洩露案例分析程序內存洩漏問題是十分常見的一類程序問題,有很多原因都會導致程序表現出內存洩漏的現象。從造成洩漏的根本原因上劃分,我們可以簡單地將問題分為兩類:▌3.1.1 程序設計不合理從分析結果來看,這類問題主要可以分為幾類:指向內存空間的指針丟失申請的內存沒有釋放或釋放不完全、內存無限申請而不釋放使用的數據結構不釋放內存
  • C 2 C++進階篇(1)
    首先談談筆者的水平,只學過c和數據結構,接觸過指針,對於取地址&從來沒有接觸過(因為據說是老師說不符合嚴謹的c....), python
  • 記一次 .NET 某市附屬醫院 Web程序 偶發性CPU爆高分析
    講故事這個月初,一位朋友加微信求助他的程序出現了 CPU 偶發性爆高,希望能有償解決一下。從描述看,這個問題應該困擾了很久,還是醫院的朋友給力,開門就是 100塊 紅包 🤣🤣🤣,那既然是偶發性爆高,人工不行,還得用 procdump 自動抓,用 procdump -ma -s 5 -n 2 -c 70 w3wp 埋伏好,幾天後如願生成了兩個dump,太妙了,接下來就用 windbg 分析吧。
  • C/C++可變參數函數
    c/c++支持可變參數的函數,即函數的參數是不確定的。一、為什麼要使用可變參數的函數?一般我們編程的時候,函數中形式參數的數目通常是確定的,在調用時要依次給出與形式參數對應的所有實際參數。但在某些情況下希望函數的參數個數可以根據需要確定,因此c語言引入可變參數函數。這也是c功能強大的一個方面,其它某些語言,比如fortran就沒有這個功能。典型的可變參數函數的例子有大家熟悉的printf()、scanf()等。二、c/c++如何實現可變參數的函數?
  • C 語言會比 C++ 快?
    和面向過程的 C 語言相比,其繼承者 C++ 不僅可以進行 C 語言的過程化程序設計,還可以進行以繼承和多態為特點的面向對象的程序設計。要論兩者上手的難易度,對此,有網友評價道,學好 C 只要 1 年,而學好 C++ 需要的可能不止 10 年。
  • Linux下C語言編譯的問題
    我們開始編譯main.c  gcc -c main.c  這時,則生成了main.o文件,然後我們再通過如下命令進行連結希望得到可執行程序。/test.a //註:./ 是給出了test.a的路徑  【擴展】:同樣,為了把問題說清楚,上面我們把代碼的編譯連結分開了,如果希望一次性生成可執行程序,則可以對main.c和test.a執行如下命令。  gcc -o main main.c .
  • C/C++程序編譯流程
    1.預處理 【g++ -E  **.cpp  -o  **.i】     預處理根據預處理指令組裝新的C/C++程序,產生一個沒有宏定義
  • 簡單的Python調用C++程序
    Python調用C/C++程序的方法 最近寫
  • Linux - cpupower調整CPU主頻
    Userspace:最早的 cpufreq 子系統通過 userspace governor 為用戶提供了這種靈活性。系統將變頻策略的決策權交給了用戶態應用程式,並提供了相應的接口供用戶態應用程式調節 CPU 運行頻率使用。
  • 一些實用的單片機c程序
    }}void program(unsigned char cpu) { unsigned int bdata adds=0; unsigned char d; switch (cpu) { case 1: //89C51 case 2: p36=p37=1;rst=1; while (1) { TH0=-100
  • 關於java單線程經常佔用cpu 100%的分析
    從jvm團隊反饋的熱點看,這個線程應該在一直獲取cpu利用率,而不是以前常見的問題。從perf的角度看,該線程一直在觸發read調用。嘗試手工讀取一下這個文件:cpu 12236554 0 0 9808893 0 0 0 0 0 0cpu0 10915814 0 0 20934 0 0 0 0 0 0cpu1 1320740 0 0 9787959 0 0 0 0 0 0說明bash訪問這個掛載點沒有問題,難道bash訪問的掛載點和出問題的
  • 九大程式語言優缺點第四期:c++
    C++:c++誕生於1983年,緊隨c語言的步伐,c++是C語言的超集,大家所知道的C語言是面向過程的,java是面向對象的,那麼C語言為了面向對象,所以誕生出現在大家所熟知的c++,被廣泛視為大規模應用構建軟體。
  • 簡要記錄丨VSCode 搭建基礎 C/C++ 編譯環境
    8        "args": [], // 程序調試時傳遞給程序的命令行參數,一般設為空即可 9        "stopAtEntry": false, // 設為true時程序將暫停在程序入口處,相當於在main上打斷點10        "cwd": "${workspaceFolder}", // 調試程序時的工作目錄,此為工作區文件夾;改成${fileDirname
  • 剖析C語言中a=a+++a的無聊問題
    同僚們閒聊,突然就聊到了a+++++a的問題。這種純屬C語言 「二」 級的問題應該是從a+++a引申出來的吧。於是乎兄弟姐妹們開始討論它的運算結果,以及改如何理解。