一文看得 Linux 性能分析|perf 原理

最近線上運行的程序出現性能問題,但通過分析程序源代碼(Code Review),並找不到導致問題的根本原因。所以,只能藉助強大的性能分析工具 perf 來找出問題所在。

perf 工具的功能非常強大,但本文並不是介紹 perf 工具的使用,而是介紹 perf 的實現原理。介紹 perf 使用的文章多如牛毛,但介紹 perf 原理和實現的卻鳳毛麟角。

但正因爲 perf 功能非常強大,所以其實現也是非常複雜的。本文只介紹其中的一個功能:分析進程中的函數調用頻率

接下來,我們先介紹怎麼使用 perf 來分析進程中的函數調用頻率。

使用 perf 分析程序性能瓶頸

在介紹 perf 的實現之前,我們先使用 perf 分析一個簡單的程序,此程序代碼如下:

// sample.c

void workload1()
{
    int i, c = 0;

    for (i = 0; i < 100000000; i++) {
        c += i * i;
        c -= i * 100;
        c += i * i * i / 100;
    }
}

void workload2()
{
    int i, c = 0;

    for (i = 0; i < 200000000; i++) {
        c += i * i;
        c -= i * 100;
        c += i * i * i / 100;
    }
}

int main(int argc, char *argv[])
{
    workload1();
    workload2();
    return 0;
}

上面的程序很簡單,我們創建兩個函數:workload1 和 workload2。從代碼可以看出,workload2 的負載是 workload1 的 2 倍。

現在我們使用 perf 來分析這個程序的性能瓶頸在哪裏。

$ gcc sample.c -g -o sample
$ sudo perf record -g ./sample sleep 10

運行上面的命令後,將會生成一個 perf.data 的文件,此文件記錄了 sample 程序運行時的採樣數據。

$ perf report -g

結果如下圖所示:

從上圖可以看出,函數 workload2(65%)的負載大概是函數 workload1(35%)的 2 倍,與我們的代碼基本一致。

perf 實現原理

通過上面的例子,我們大概知道怎麼使用 perf 來分析程序的性能瓶頸。接下來,我們將會介紹 perf 的內部實現原理。

來思考一下,如果讓我們來設計一個統計程序中各個函數佔用 CPU 時間的方案,應該如何設計?最簡單的方案就是:在各個函數的開始記錄當前時間,然後在函數執行結束後,使用當前時間減去函數開始執行時的時間,得到函數的執行時間總時長。如下僞代碼:

void func1()
{
    ...
}

void func2()
{
    ...
}

int main(int argc, char *argv[])
{
    int start_time, total_time;
  
    start_time = now();
    func1();
    total_time = now() - start_time;
    printf("func1() spent %d\n", total_time);

    start_time = now();
    func2();
    total_time = now() - start_time;
    printf("func2() spent %d\n", total_time);
}

雖然上述方式可以統計程序中各個函數的耗時情況,但卻存在很多問題:

  1. 代碼入侵度高。由於要對每個函數進行耗時記錄,所以必須在調用函數前和調用函數後加入統計代碼。

  2. 統計函數耗時,並不能反映該函數的真實 CPU 使用率。比如函數內部調用了導致進程休眠的系統調用(如 sleep),這時函數實際上是不使用 CPU 的,但函數的耗時卻統計了休眠的時間。

  3. 對性能影響較大。由於程序中所有函數都加入統計代碼,所以對性能的影響是非常大的。

所以我們需要一個系統,它能夠避免上述問題:

  1. 零代碼入侵。

  2. 能夠真實反映函數的 CPU 使用率。

  3. 對性能影響較小。

perf 就是爲了解決上述問題而生的,我們先來介紹一下 perf 的原理。

採樣

爲了減小對程序性能的影響,perf 並不會在每個函數加入統計代碼,取而代之的統計方式是:採樣。

採樣的原理是:設置一個定時器,當定時器觸發時,查看當前進程正在執行的函數,然後記錄下來。如下圖所示:

如上圖所示,每個 cpu-clock 是一個定時器的觸發點。在 6 次定時器觸發點中,函數 func1 被命中了 3 次,函數 func2 被命中了 1 次,函數 func3 被命中了 2 次。所以,我們可以推測出,函數 func1 的 CPU 使用率最高。

排序

如果程序有成千上萬的函數,那麼採樣出來的數據可能非常多,這個時候就需要對採樣的數據進行排序。

爲了對採樣數據進行排序,perf 使用紅黑樹這種數據結構,如下圖所示:

如上圖所示,在 perf 採樣的數據中,有 7 個函數被統計了命中次數,perf 使用採樣到的數據構建一棵紅黑樹。

根據紅黑樹的特性,最右邊的節點就是被命中最多的函數,這樣就能把程序中 CPU 使用率最高的函數找出來。

總結

由於 perf 的功能非常強大,所以本文也只介紹了 perf 其中一種功能:統計函數的 CPU 使用率。

在下一篇文章中,我們將會介紹 perf 的代碼實現。Linux 的創始人 Linus 曾經說過:Read the f**king source code,要真正理解一個系統,只能通過閱讀其源碼。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Vj418ZwVe6PxE1erDu8bkg