valgrind和Kcachegrind效能分析工具詳解

Y發表於2021-02-01

一、valgrind介紹

valgrind是執行在Linux上的一套基於模擬技術的程式除錯和分析工具,用於構建動態分析工具的裝備性框架。它包括一個工具集,每個工具執行某種型別的除錯、分析或類似的任務,以幫助完善你的程式。Valgrind的架構是模組化的,所以可以容易的建立新的工具而又不會擾亂現有的結構。

valgrind主要包含以下工具:

1、memcheck:檢查程式中的記憶體問題,如洩漏、越界、非法指標等。

2、callgrind:檢測程式程式碼的執行時間和呼叫過程,以及分析程式效能。

3、cachegrind:分析CPU的cache命中率、丟失率,用於進行程式碼優化。

4、helgrind:用於檢查多執行緒程式的競態條件。

5、massif:堆疊分析器,指示程式中使用了多少堆記憶體等資訊。

另外,也有一些大多數使用者不會用到的小工具: Lackey是一個示例工具,用於演示一些裝備的基礎性內容;Nulgrind是一個最小化的Valgrind工具,不做分析或者操作,僅用於測試目的。

二、valgrind安裝及使用

安裝

建議從valgrind官網下載安裝,目前官網的最新包是3.16.1

$ mkdir valgrind-inst
$ cd valgrind-inst/
$ wget https://sourceware.org/pub/valgrind/valgrind-3.16.1.tar.bz2

$ ls
valgrind-3.16.1.tar.bz2

解壓後進行安裝,可以指定安裝目錄,這樣的話記得設定環境變數

$ tar -xvf valgrind-3.16.1.tar.bz2
$ cd valgrind-3.16.1
$ ./configure --prefix=/usr/local/valgrind
$ make
$ make install

檢視是否安裝成功

$ valgrind --version
valgrind-3.16.1
工具集的使用

基本使用格式如下:

usage: valgrind [options] prog-and-args

其支援眾多選項,我們可以通過valgrind --help來進行檢視。

這裡我們只介紹幾個較為常用的選項


--tool: 是最常用的選項,用於選擇使用valgrind工具集中的哪一個工具。預設值為memcheck。

--version: 用於列印valgrind的版本號

-q/--quiet: 安靜的執行,只列印錯誤訊息;

-v/--verbose: 列印更詳細的資訊;

--trace-children: 是否跟蹤子程式,預設值為no;

--track-fds: 是否追蹤開啟的檔案描述符,預設為no

--time-stamp=no|yes: 是否在列印出的每條訊息之前加上時間戳資訊。預設值為no

--log-file=<file>: 指定將訊息列印到某個檔案

--default-suppressions: 載入預設的抑制引數。

--alignment: 指定malloc分配記憶體時的最小對齊位元組數;

如下的一些選項用於Memcheck工具:

--leak-check=no|summary|full: 在退出時是否查詢記憶體洩露。預設值為summary

--show-leak-kinds=kind1,kind2,..: 顯示哪一種型別的記憶體洩露。預設顯示definite和possible這兩種;

三、 Valgrind 工具詳解

1) memcheck

最常用的工具,用來檢測程式中出現的記憶體問題,所有對記憶體的讀寫都會被檢測到,一切對mallocfreenewdelete的呼叫都會被捕獲。所以,它能檢測以下問題:

1、使用未初始化的記憶體。如果在定義一個變數時沒有賦初始值,後邊即使賦值了,使用這個變數的時候Memcheck也會報"uninitialised value"錯誤。使用中會發現,valgrind提示很多這個錯誤,由於關注的是記憶體洩漏問題,所以可以用--undef-value-errors=選項把這個錯誤提示遮蔽掉,具體可以看後面的選項解釋。

2、讀/寫釋放後的記憶體塊;

3、記憶體讀寫越界(陣列訪問越界/訪問已經釋放的記憶體),讀/寫超出malloc分配的記憶體塊;

4、讀/寫不適當的棧中記憶體塊;

5、記憶體洩漏,指向一塊記憶體的指標永遠丟失;

6、不正確的malloc/free或new/delete匹配(重複釋放/使用不匹配的分配和釋放函式);

7、記憶體覆蓋,memcpy()相關函式中的dst和src指標重疊。
   

用法:

將程式編譯生成可執行檔案後執行:valgrind –leak-check=full ./程式名

注意:下面討論的所有測試程式碼在編譯時最好都加上-g選項(用來在memcheck的輸出中生成行號)進行編譯。

測試程式驗證:

編寫測試程式

#include <stdlib.h>

void func() {
    char *p = new char[10];
}

int main() {
    func();

    return 0;
}

編譯後,用valgrind檢測程式。
如果設定了--leak-check=fullMemcheck會給出詳細的每個塊是在哪裡分配,並且給出分配時函式呼叫堆疊(編譯的時候使用-g選項和去掉-o優化選項,就可以得到更詳細的函式資訊,可以精確到程式碼的某一行)。可以通過--show-leak-kinds選項來選擇要詳細報告哪幾種型別的錯誤。Memcheck會把函式呼叫堆疊相同或相似的記憶體塊資訊,放到同一個條目來顯示,可以通過--leak-resolution來控制這個"相似"判斷的力度。

$ g++ -g -o test leak.cpp
$ valgrind --tool=memcheck --leak-check=full ./test

檢測結果如下:

==6018== Memcheck, a memory error detector
==6018== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==6018== Using Valgrind-3.16.1 and LibVEX; rerun with -h for copyright info
==6018== Command: ./test
==6018== 
==6018== 
==6018== HEAP SUMMARY:
==6018==     in use at exit: 10 bytes in 1 blocks
==6018==   total heap usage: 1 allocs, 0 frees, 10 bytes allocated
==6018== 
==6018== 10 bytes in 1 blocks are definitely lost in loss record 1 of 1
==6018==    at 0x4C2AC58: operator new[](unsigned long) (vg_replace_malloc.c:431)
==6018==    by 0x40062E: func() (leak.cpp:4)
==6018==    by 0x40063D: main (leak.cpp:8)
==6018== 
==6018== LEAK SUMMARY:
==6018==    definitely lost: 10 bytes in 1 blocks
==6018==    indirectly lost: 0 bytes in 0 blocks
==6018==      possibly lost: 0 bytes in 0 blocks
==6018==    still reachable: 0 bytes in 0 blocks
==6018==         suppressed: 0 bytes in 0 blocks
==6018== 
==6018== For lists of detected and suppressed errors, rerun with: -s
==6018== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

結果說明:

先看看輸出資訊中的HEAP SUMMARY,它表示程式在堆上分配記憶體的情況,其中的1 allocs
表示程式分配了 1 次記憶體,0 frees表示程式釋放了 0 次記憶體,10 bytes allocated表示分配了 10 個位元組的記憶體。
另外,Valgrind 也會報告程式是在哪個位置發生記憶體洩漏。

上面LEAK SUMMARY會列印5種不同的型別,這裡我們簡單介紹一下:

definitely lost: 明確丟失的記憶體。程式中存在記憶體洩露,應儘快修復。當程式結束時如果一塊動態分配的記憶體沒有被釋放並且通過程式內的指標變數均無法訪問這塊記憶體則會報這個錯誤;

indirectly lost: 間接丟失。當使用了含有指標成員的類或結構體時可能會報這個錯誤。這類錯誤無需直接修復,它們總是與definitely lost一起出現,只要修復definitely lost即可。

possibly lost: 可能丟失。大多數情況下應視為與definitely lost一樣需要儘快修復,除非你的程式讓一個指標指向一塊動態分配的記憶體(但不是這塊記憶體的起始地址),然後通過運算得到這塊記憶體的起始地址,再釋放它。當程式結束時如果一塊動態分配的記憶體沒有被釋放並且通過程式內的指標變數均無法訪問這塊記憶體的起始地址,但可以訪問其中的某一部分資料,則會報這個錯誤。

stil reachable: 可以訪問,未丟失但也未釋放。如果程式是正常結束的,那麼它可能不會造成程式崩潰,但長時間執行有可能耗盡系統資源。

其他幾種情況,寫一個綜合的測試程式進行驗證。

// mixed.cpp

void func() {
    char *ptr = new char[10];
    ptr[10] = 'a';   // 記憶體越界

    memcpy(ptr + 1, ptr, 5);   // 踩記憶體

    delete []ptr;
    delete []ptr; // 重複釋放

    char *p;
    *p = 1;   // 非法指標
}

int main() {
    func();

    return 0;
}

編譯後,用valgrind檢測程式。

$ g++ -g -o test mixed.cpp
$ valgrind --tool=memcheck --leak-check=full ./test

檢測結果

==22786== Memcheck, a memory error detector
==22786== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==22786== Using Valgrind-3.16.1 and LibVEX; rerun with -h for copyright info
==22786== Command: ./test
==22786== 
==22786== Invalid write of size 1      // 記憶體越界
==22786==    at 0x4007FB: func() (mixed.cpp:6)
==22786==    by 0x400851: main (mixed.cpp:18)
==22786==  Address 0x5a2404a is 0 bytes after a block of size 10 alloc'd
==22786==    at 0x4C2AC58: operator new[](unsigned long) (vg_replace_malloc.c:431)
==22786==    by 0x4007EE: func() (mixed.cpp:5)
==22786==    by 0x400851: main (mixed.cpp:18)
==22786== 
==22786== Source and destination overlap in memcpy(0x5a24041, 0x5a24040, 5)  // 踩記憶體
==22786==    at 0x4C2E83D: memcpy@@GLIBC_2.14 (vg_replace_strmem.c:1033)
==22786==    by 0x400819: func() (mixed.cpp:8)
==22786==    by 0x400851: main (mixed.cpp:18)
==22786== 
==22786== Invalid free() / delete / delete[] / realloc()    // 重複釋放
==22786==    at 0x4C2BBAF: operator delete[](void*) (vg_replace_malloc.c:649)
==22786==    by 0x40083F: func() (mixed.cpp:11)
==22786==    by 0x400851: main (mixed.cpp:18)
==22786==  Address 0x5a24040 is 0 bytes inside a block of size 10 free'd
==22786==    at 0x4C2BBAF: operator delete[](void*) (vg_replace_malloc.c:649)
==22786==    by 0x40082C: func() (mixed.cpp:10)
==22786==    by 0x400851: main (mixed.cpp:18)
==22786==  Block was alloc'd at
==22786==    at 0x4C2AC58: operator new[](unsigned long) (vg_replace_malloc.c:431)
==22786==    by 0x4007EE: func() (mixed.cpp:5)
==22786==    by 0x400851: main (mixed.cpp:18)
==22786== 
==22786== Use of uninitialised value of size 8    // 非法指標
==22786==    at 0x400844: func() (mixed.cpp:14)
==22786==    by 0x400851: main (mixed.cpp:18)
==22786== 
==22786== 
==22786== Process terminating with default action of signal 11 (SIGSEGV): dumping core
==22786==  Bad permissions for mapped region at address 0x4008B0
==22786==    at 0x400844: func() (mixed.cpp:14)
==22786==    by 0x400851: main (mixed.cpp:18)
==22786== 
==22786== HEAP SUMMARY:
==22786==     in use at exit: 0 bytes in 0 blocks
==22786==   total heap usage: 1 allocs, 2 frees, 10 bytes allocated
==22786== 
==22786== All heap blocks were freed -- no leaks are possible
==22786== 
==22786== Use --track-origins=yes to see where uninitialised values come from  
==22786== For lists of detected and suppressed errors, rerun with: -s
==22786== ERROR SUMMARY: 4 errors from 4 contexts (suppressed: 0 from 0)
Segmentation fault (core dumped)

可見valgrind將上述幾種情況都檢測出來了。

2) Callgrind

gprof類似的分析工具,但它對程式的執行觀察更為入微,能給我們提供更多的資訊。和gprof不同的是,它不需要在編譯原始碼時附加特殊選項,但還是推薦加上除錯選項。Callgrind收集程式執行時的一些資料,建立函式呼叫關係圖,還可以有選擇地進行cache模擬。在執行結束時,它會把分析資料寫入一個檔案。callgrind_annotate可以把這個檔案的內容轉化成可讀的形式。

測試程式

#include <stdio.h>
#include <unistd.h>

void test() {
    sleep(1);
}
void func() {
    for(int i = 0; i < 10; i++) {
        test();
    }
}

int main() {
    func();
    printf("process is over!\n");

    return 0;
}

編譯後,用valgrind檢測程式。

$ g++ -g -o test callgrind.cpp
$ valgrind --tool=callgrind ./test
$ ls
callgrind.cpp  callgrind.out.3490  test

callgrind.out.3490就是callgrind生成的檔案。

這裡介紹一個圖形化效能分析工具Kcachegrind

Kcachegrind官網地址

下載安裝後可以用來分析callgrind生成的檔案。

Kcachegrind開啟callgrind.out.3490這個檔案,如下圖:

通過圖形化,我們可以很直觀的知道哪段程式執行慢,並且瞭解相關呼叫關係。

3) Cachegrind

Cache分析器,它模擬CPU中的一級緩存和二級快取,能夠精確地指出程式中cache的丟失和命中。如果需要,它還能夠為我們提供cache丟失次數,記憶體引用次數,以及每行程式碼,每個函式,每個模組,整個程式產生的指令數。這對優化程式有很大的幫助。

它的使用方法也是:valgrind –tool=cachegrind ./程式名

4) Helgrind

它主要用來檢查多執行緒程式中出現的競爭問題。Helgrind尋找記憶體中被多個執行緒訪問,而又沒有一貫加鎖的區域,這些區域往往是執行緒之間失去同步的地方,而且會導致難以發覺的錯誤。Helgrind實現了名為Eraser的競爭檢測演算法,並做了進一步改進,減少了報告錯誤的次數。不過,Helgrind仍然處於實驗狀態。

測試程式碼:

#include <stdio.h>
#include <pthread.h>

#define NUM 10
int counter = 0;

void *threadfunc(void*) {
    for (int i = 0; i < NUM; i++) {
        counter += i;
    }
}

int main() {
    pthread_t tid1, tid2;

    pthread_create(&tid1, NULL, &threadfunc, NULL);
    pthread_create(&tid2, NULL, &threadfunc, NULL);

    // wait for thread to terminate
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    printf("counter = %d\n", counter);

    return 0;
}

編譯後,用valgrind檢測程式。

$ g++ -g -o test helgrind.cpp -lpthread
$ valgrind --tool=helgrind ./test

檢測結果:

==27722== Helgrind, a thread error detector
==27722== Copyright (C) 2007-2017, and GNU GPL'd, by OpenWorks LLP et al.
==27722== Using Valgrind-3.16.1 and LibVEX; rerun with -h for copyright info
==27722== Command: ./test
==27722== 
==27722== ---Thread-Announcement------------------------------------------
==27722== 
==27722== Thread #3 was created
==27722==    at 0x597589E: clone (in /usr/lib64/libc-2.17.so)
==27722==    by 0x4E43059: do_clone.constprop.4 (in /usr/lib64/libpthread-2.17.so)
==27722==    by 0x4E44569: pthread_create@@GLIBC_2.2.5 (in /usr/lib64/libpthread-2.17.so)
==27722==    by 0x4C30CFA: pthread_create_WRK (hg_intercepts.c:425)
==27722==    by 0x4C31DD8: pthread_create@* (hg_intercepts.c:458)
==27722==    by 0x400728: main (helgrind.cpp:17)
==27722== 
==27722== ---Thread-Announcement------------------------------------------
==27722== 
==27722== Thread #2 was created
==27722==    at 0x597589E: clone (in /usr/lib64/libc-2.17.so)
==27722==    by 0x4E43059: do_clone.constprop.4 (in /usr/lib64/libpthread-2.17.so)
==27722==    by 0x4E44569: pthread_create@@GLIBC_2.2.5 (in /usr/lib64/libpthread-2.17.so)
==27722==    by 0x4C30CFA: pthread_create_WRK (hg_intercepts.c:425)
==27722==    by 0x4C31DD8: pthread_create@* (hg_intercepts.c:458)
==27722==    by 0x40070D: main (helgrind.cpp:16)
==27722== 
==27722== ----------------------------------------------------------------
==27722== 
==27722== Possible data race during read of size 4 at 0x601048 by thread #3
==27722== Locks held: none
==27722==    at 0x4006CE: threadfunc(void*) (helgrind.cpp:9)
==27722==    by 0x4C30EEE: mythread_wrapper (hg_intercepts.c:387)
==27722==    by 0x4E43EA4: start_thread (in /usr/lib64/libpthread-2.17.so)
==27722==    by 0x59758DC: clone (in /usr/lib64/libc-2.17.so)
==27722== 
==27722== This conflicts with a previous write of size 4 by thread #2
==27722== Locks held: none
==27722==    at 0x4006D9: threadfunc(void*) (helgrind.cpp:9)
==27722==    by 0x4C30EEE: mythread_wrapper (hg_intercepts.c:387)
==27722==    by 0x4E43EA4: start_thread (in /usr/lib64/libpthread-2.17.so)
==27722==    by 0x59758DC: clone (in /usr/lib64/libc-2.17.so)
==27722==  Address 0x601048 is 0 bytes inside data symbol "counter"
==27722== 
==27722== ----------------------------------------------------------------
==27722== 
==27722== Possible data race during write of size 4 at 0x601048 by thread #3
==27722== Locks held: none
==27722==    at 0x4006D9: threadfunc(void*) (helgrind.cpp:9)
==27722==    by 0x4C30EEE: mythread_wrapper (hg_intercepts.c:387)
==27722==    by 0x4E43EA4: start_thread (in /usr/lib64/libpthread-2.17.so)
==27722==    by 0x59758DC: clone (in /usr/lib64/libc-2.17.so)
==27722== 
==27722== This conflicts with a previous write of size 4 by thread #2
==27722== Locks held: none
==27722==    at 0x4006D9: threadfunc(void*) (helgrind.cpp:9)
==27722==    by 0x4C30EEE: mythread_wrapper (hg_intercepts.c:387)
==27722==    by 0x4E43EA4: start_thread (in /usr/lib64/libpthread-2.17.so)
==27722==    by 0x59758DC: clone (in /usr/lib64/libc-2.17.so)
==27722==  Address 0x601048 is 0 bytes inside data symbol "counter"
==27722== 
counter = 90
==27722== 
==27722== Use --history-level=approx or =none to gain increased speed, at
==27722== the cost of reduced accuracy of conflicting-access information
==27722== For lists of detected and suppressed errors, rerun with: -s
==27722== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

從上述結果知道,valgrind分析出了競態的情況。

5) Massif

堆疊分析器,它能測量程式在堆疊中使用了多少記憶體,告訴我們堆塊,堆管理塊和棧的大小。Massif能幫助我們減少記憶體的使用,在帶有虛擬記憶體的現代系統中,它還能夠加速我們程式的執行,減少程式停留在交換區中的機率。

Massif對記憶體的分配和釋放做profile。程式開發者通過它可以深入瞭解程式的記憶體使用行為,從而對記憶體使用進行優化。這個功能對C++尤其有用,因為C++有很多隱藏的記憶體分配和釋放。

此外,lackeynulgrind 也會提供。Lackey 是小型工具,很少用到;Nulgrind 只是為開發者展示如何建立一個工具。我們就不做介紹了。

參考列表

valgrind的使用

Linux 下利用 valgrind工具進行記憶體洩露檢測和效能分析

valgrind詳解與使用例項

使用 Valgrind 檢測 C++記憶體洩漏

利用效能分析工具valgrind+KCachegrind分析

Linux效能分析工具與圖形化方法

相關文章