自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

剖析CPU性能火焰圖生成的內(nèi)部原理

商務(wù)辦公
火焰圖是一個非常好的用于分析熱點函數(shù)的工具,只要你關(guān)注性能優(yōu)化,就應(yīng)該學(xué)會使用它來分析你的程序。我們今天的文章不光是介紹了火焰圖是如何生成的,而且還介紹了其底層的工作原理?;鹧鎴D的生成主要分兩步,一是采樣,而是渲染。

大家好,我是飛哥!

在進(jìn)行CPU性能優(yōu)化的時候,我們經(jīng)常先需要分析出來我們的應(yīng)用程序中的CPU資源在哪些函數(shù)中使用的比較多,這樣才能高效地優(yōu)化。一個非常好的分析工具就是《性能之巔》作者 Brendan Gregg 發(fā)明的火焰圖。

圖片

我們今天就來介紹下火焰圖的使用方法,以及它的工作原理。

一、火焰圖的使用

為了更好地展示火焰圖的原理,我專門寫了一小段代碼,

int main() {
    for (i = 0; i < 100; i++) {
        if (i < 10) {
            funcA();
        } else if (i < 16) {
            funcB();
        } else {
            funcC();
        }
    }
}

完整的源碼我放到了咱們開發(fā)內(nèi)功修煉的Github上了:https://github.com/yanfeizhang/coder-kung-fu/blob/main/tests/cpu/test09/main.c。

接下來我們用這個代碼實際體驗一下火焰圖是如何生成的。在本節(jié)中,我們只講如何使用,原理后面的小節(jié)再展開。

# gcc -o main main.c
# perf record -g ./main

這個時候,在你執(zhí)行命令的當(dāng)前目錄下生成了一個perf.data文件。接下來咱們需要把Brendan Gregg的生成火焰圖的項目下載下來。我們需要這個項目里的兩個perl腳本。

# git clone https://github.com/brendangregg/FlameGraph.git

接下來我們使用 perf script 解析這個輸出文件,并把輸出結(jié)果傳入到 FlameGraph/stackcollapse-perf.pl 腳本中來進(jìn)一步解析,最后交由 FlameGraph/flamegraph.pl 來生成svg 格式的火焰圖。具體命令可以一行來完成。

# perf script | ./FlameGraph/stackcollapse-perf.pl | ./FlameGraph/flamegraph.pl > out.svg

這樣,一副火焰圖就生成好了。

圖片

之所以選擇我提供一個 demo 代碼來生成,是因為這個足夠簡單和清晰,方便大家理解。在上面這個火焰圖中,可以看出 main 函數(shù)調(diào)用了 funcA、funcB、funcC,其中 funcA 又調(diào)用了 funcD、funcE,然后這些函數(shù)的開銷又都不是自己花掉的,而是因為自己調(diào)用的一個 CPU 密集型的函數(shù) caculate。整個系統(tǒng)的調(diào)用棧的耗時統(tǒng)計就十分清晰的展現(xiàn)在眼前了。

如果要對這個項目進(jìn)行性能優(yōu)化。在上方的火焰圖中看雖然funcA、funcB、funcC、funcD、funcE這幾個函數(shù)的耗時都挺長,但它們的耗時并不是自己用掉的。而且都花在執(zhí)行子函數(shù)里了。我們真正應(yīng)該關(guān)注的是火焰圖最上方 caculate 這種又長又平的函數(shù)。因為它才是真正花掉 CPU 時間的代碼。其它項目中也一樣,拿到火焰圖后,從最上方開始,把耗時比較長的函數(shù)找出來,優(yōu)化掉。

另外就是在實際的項目中,可能函數(shù)會非常的多,并不像上面這么簡單,很多函數(shù)名可能都被折疊起來了。這個也好辦,svg 格式的圖片是支持交互的,你可以點擊其中的某個函數(shù),然后就可以展開了只詳細(xì)地看這個函數(shù)以及其子函數(shù)的火焰圖了。

怎么樣,火焰圖使用起來是不是還挺簡單的。接下來的小節(jié)中我們再來講講火焰圖生成全過程的內(nèi)部原理。理解了這個,你才能講火焰圖用的得心應(yīng)手。

二、perf采樣

2.1 perf 介紹

在生成火焰圖的第一步中,就是需要對你要觀察的進(jìn)程或服務(wù)器進(jìn)行采樣。采樣可用的工具有好幾個,我們這里用的是 perf record。

# perf record -g ./main

上面的命令中 -g 指的是采樣的時候要記錄調(diào)用棧的信息。./main 是啟動 main 程序,并只采樣這一個進(jìn)程。這只是個最簡單的用法,其實 perf record 的功能非常的豐富。

它可以指定采集事件。當(dāng)前系統(tǒng)支持的事件列表可以用過 perf list 來查看。默認(rèn)情況下采集的是 Hardware event 下的 cycles 這一事件。假如我們想采樣 cache-misses 事件,我們可以通過 -e 參數(shù)指定。

# perf record -e cache-misses  sleep 5 // 指定要采集的事件

還可以指定采樣的方式。該命令支持兩種采樣方式,時間頻率采樣,事件次數(shù)發(fā)生采樣。-F 參數(shù)指定的是每秒鐘采樣多少次。-c參數(shù)指定的是每發(fā)生多少次采樣一次。

# perf record -F 100 sleep 5           // 每一秒鐘采樣100次
# perf record -c 100 sleep 5           // 每發(fā)生100次采樣一次

還可以指定要記錄的CPU核

# perf record -C 0,1 sleep 5           // 指定要記錄的CPU號
# perf record -C 0-2 sleep 5           // 指定要記錄的CPU范圍

還可以采集內(nèi)核的調(diào)用棧

# perf record -a -g ./main

在使用 perf record 執(zhí)行后,會將采樣到的數(shù)據(jù)都生成到 perf.data 文件中。在上面的實驗中,雖然我們只采集了幾秒,但是生成的文件還挺大的,有 800 多 KB。我們通過 perf script 命令可以解析查看一下該文件的內(nèi)容。大概有 5 萬多行。其中的內(nèi)容就是采樣 cycles 事件時的調(diào)用棧信息。

......
59848 main 412201 389052.225443:     676233 cycles:u:
59849             55651b8b5132 caculate+0xd (/data00/home/zhangyanfei.allen/work_test/test07/main)
59850             55651b8b5194 funcC+0xe (/data00/home/zhangyanfei.allen/work_test/test07/main)
59851             55651b8b51d6 main+0x3f (/data00/home/zhangyanfei.allen/work_test/test07/main)
59852             7f8987d6709b __libc_start_main+0xeb (/usr/lib/x86_64-linux-gnu/libc-2.28.so)
59853         41fd89415541f689 [unknown] ([unknown])
......

除了 perf script 外,還可以使用 perf report 來查看和渲染結(jié)果。

# perf report -n --stdio

圖片

2.2 內(nèi)核工作過程

我們來簡單看一下內(nèi)核是如何工作的。

perf在采樣的過程大概分為兩步,一是調(diào)用 perf_event_open 來打開一個 event 文件,而是調(diào)用 read、mmap等系統(tǒng)調(diào)用讀取內(nèi)核采樣回來的數(shù)據(jù)。整體的工作流程圖大概如下

圖片

其中 perf_event_open 完成了非常重要的幾項工作。

  • 創(chuàng)建各種event內(nèi)核對象
  • 創(chuàng)建各種event文件句柄
  • 指定采樣處理回調(diào)

我們來看下它的幾個關(guān)鍵執(zhí)行過程。在 perf_event_open 調(diào)用的 perf_event_alloc 指定了采樣處理回調(diào)函數(shù)為,比如perf_event_output_backward、perf_event_output_forward等

static struct perf_event *
perf_event_alloc(struct perf_event_attr *attr, ...)
{   
    ...
    if (overflow_handler) {
        event->overflow_handler = overflow_handler;
        event->overflow_handler_context = context;
    } else if (is_write_backward(event)){
        event->overflow_handler = perf_event_output_backward;
        event->overflow_handler_context = NULL;
    } else {
        event->overflow_handler = perf_event_output_forward;
        event->overflow_handler_context = NULL;
    }
    ...
}

當(dāng) perf_event_open 創(chuàng)建事件對象,并打開后,硬件上發(fā)生的事件就可以出發(fā)執(zhí)行了。內(nèi)核注冊相應(yīng)的硬件中斷處理函數(shù)是 perf_event_nmi_handler。

//file:arch/x86/events/core.c
register_nmi_handler(NMI_LOCAL, perf_event_nmi_handler, 0, "PMI");

這樣 CPU 硬件會根據(jù) perf_event_open 調(diào)用時指定的周期發(fā)起中斷,調(diào)用 perf_event_nmi_handler 通知內(nèi)核進(jìn)行采樣處理

//file:arch/x86/events/core.c
static int perf_event_nmi_handler(unsigned int cmd, struct pt_regs *regs)
{    
    ret = x86_pmu.handle_irq(regs);
    ...
}

該終端處理函數(shù)的函數(shù)調(diào)用鏈經(jīng)過 x86_pmu_handle_irq 到達(dá) perf_event_overflow。其中 perf_event_overflow 是一個關(guān)鍵的采樣函數(shù)。無論是硬件事件采樣,還是軟件事件采樣都會調(diào)用到它。它會調(diào)用 perf_event_open 時注冊的 overflow_handler。我們假設(shè) overflow_handler 為 perf_event_output_forward

void
perf_event_output_forward(struct perf_event *event, ...)
{
    __perf_event_output(event, data, regs, perf_output_begin_forward);
}

在 __perf_event_output 中真正進(jìn)行了采樣處理

//file:kernel/events/core.c
static __always_inline int
__perf_event_output(struct perf_event *event, ...)
{
    ...
    // 進(jìn)行采樣
    perf_prepare_sample(&header, data, event, regs);
    // 保存到環(huán)形緩存區(qū)中
    perf_output_sample(&handle, &header, data, event);
}

如果開啟了 PERF_SAMPLE_CALLCHAIN,則不僅僅會把當(dāng)前在執(zhí)行的函數(shù)名采集下來,還會把整個調(diào)用鏈都記錄起來。

//file:kernel/events/core.c
void perf_prepare_sample(...)
{

    //1.采集IP寄存器,當(dāng)前正在執(zhí)行的函數(shù)
    if (sample_type & PERF_SAMPLE_IP)
        data->ip = perf_instruction_pointer(regs);

    //2.采集當(dāng)前的調(diào)用鏈
    if (sample_type & PERF_SAMPLE_CALLCHAIN) {
        int size = 1;

        if (!(sample_type & __PERF_SAMPLE_CALLCHAIN_EARLY))
            data->callchain = perf_callchain(event, regs);

        size += data->callchain->nr;

        header->size += size * sizeof(u64);
    }
    ...
}

這樣硬件和內(nèi)核一起協(xié)助配合就完成了函數(shù)調(diào)用棧的采樣。后面 perf 工具就可以讀取這些數(shù)據(jù)并進(jìn)行下一次的處理了。

三、FlameGraph工作過程

前面我們用 perf script 解析是看到的函數(shù)調(diào)用棧信息比較的長。

......
59848 main 412201 389052.225443:     676233 cycles:u:
59849             55651b8b5132 caculate+0xd (/data00/home/zhangyanfei.allen/work_test/test07/main)
59850             55651b8b5194 funcC+0xe (/data00/home/zhangyanfei.allen/work_test/test07/main)
59851             55651b8b51d6 main+0x3f (/data00/home/zhangyanfei.allen/work_test/test07/main)
59852             7f8987d6709b __libc_start_main+0xeb (/usr/lib/x86_64-linux-gnu/libc-2.28.so)
59853         41fd89415541f689 [unknown] ([unknown])
......

在畫火焰圖的前一步得需要對這個數(shù)據(jù)進(jìn)行一下預(yù)處理。stackcollapse-perf.pl 腳本會統(tǒng)計每個調(diào)用棧回溯出現(xiàn)的次數(shù),并將調(diào)用棧處理為一行。行前面表示的是調(diào)用棧,后面輸出的是采樣到該函數(shù)在運行的次數(shù)。

# perf script | ../FlameGraph/stackcollapse-perf.pl
main;[unknown];__libc_start_main;main;funcA;funcD;funcE;caculate 554118432
main;[unknown];__libc_start_main;main;funcB;caculate 338716787
main;[unknown];__libc_start_main;main;funcC;caculate 4735052652
main;[unknown];_dl_sysdep_start;dl_main;_dl_map_object_deps 9208
main;[unknown];_dl_sysdep_start;init_tls;[unknown] 29747
main;_dl_map_object;_dl_map_object_from_fd 9147
main;_dl_map_object;_dl_map_object_from_fd;[unknown] 3530
main;_start 273
main;version_check_doit 16041

上面 perf script 5 萬多行的輸出,經(jīng)過 stackcollapse.pl 預(yù)處理后,輸出只有不到 10 行。數(shù)據(jù)量大大地得到了簡化。在 FlameGraph 項目目錄下,能看到好多 stackcollapse 開頭的文件

圖片

這是因為各種語言、各種工具采樣輸出是不一樣的,所以自然也就需要不同的預(yù)處理腳本來解析。

在經(jīng)過 stackcollapse 處理得到了上面的輸出結(jié)果后,就可以開始畫火焰圖了。flamegraph.pl 腳本工作原理是:將上面的一行繪制成一列,采樣數(shù)得到的次數(shù)越大列就越寬。另外就是如果同一級別如果函數(shù)名一樣,就合并到一起。比如現(xiàn)在有一下數(shù)據(jù)文件:

funcA;funcB;funcC 2
funcA; 1
funcD; 1

我可以通過手工畫一個類似的火焰圖,如下:

圖片

其中 funcA 因為兩行記錄合并,所以占據(jù)了 3 的寬度。funcD 沒有合并,占據(jù)就是1。另外 funcB、funcC都畫在A上的上方,占據(jù)的寬度都是2。

總結(jié)

火焰圖是一個非常好的用于分析熱點函數(shù)的工具,只要你關(guān)注性能優(yōu)化,就應(yīng)該學(xué)會使用它來分析你的程序。我們今天的文章不光是介紹了火焰圖是如何生成的,而且還介紹了其底層的工作原理?;鹧鎴D的生成主要分兩步,一是采樣,而是渲染。

在采樣這一步,主要依賴的是內(nèi)核提供的 perf_event_open 系統(tǒng)調(diào)用。該系統(tǒng)調(diào)用在內(nèi)部進(jìn)行了非常復(fù)雜的處理過程。最終內(nèi)核和硬件一起協(xié)同合作,會定時將當(dāng)前正在執(zhí)行的函數(shù),以及函數(shù)完整的調(diào)用鏈路都給記錄下來。

在渲染這一步,Brendan Gregg 提供的腳本會出 perf 工具輸出的 perf_data 文件進(jìn)行預(yù)處理,然后基于預(yù)處理后的數(shù)據(jù)渲染成 svg 圖片。函數(shù)執(zhí)行的次數(shù)越多,在 svg 圖片中的寬度就越寬。我們就可以非常直觀地看出哪些函數(shù)消耗的 CPU 多了。

最后再補(bǔ)充說一句是,我們的火焰圖只是一個采樣的渲染結(jié)果,并不一定完全代表真實情況,但也夠用了。

責(zé)任編輯:武曉燕 來源: 開發(fā)內(nèi)功修煉
相關(guān)推薦

2023-12-31 19:41:04

PHP性能終端

2009-09-14 10:35:15

Linq內(nèi)部執(zhí)行原理

2022-07-14 08:02:57

Netty網(wǎng)絡(luò)模塊

2016-09-02 12:34:52

2019-12-06 11:18:07

LinuxCPU架構(gòu)

2025-02-04 10:58:16

2009-12-31 11:37:05

MPLS網(wǎng)絡(luò)

2015-07-28 10:06:03

C#內(nèi)部實現(xiàn)剖析

2016-12-19 14:35:32

Spark Strea原理剖析數(shù)據(jù)

2010-09-06 12:50:09

PPP鏈路

2021-02-02 08:11:50

火焰圖組件技術(shù)

2010-03-01 16:48:02

Python模塊

2010-06-29 11:00:25

UML類圖實例

2010-06-12 14:35:46

UML對象圖

2023-06-07 15:25:19

Kafka版本日志

2009-09-22 15:22:08

Hibernate性能

2010-02-04 15:38:39

Android 手機(jī)

2021-04-22 09:20:20

KubernetesKubectl FlaLinux

2021-05-07 09:18:04

CSS 文字動畫技巧

2023-09-18 23:37:50

Kubernetes架構(gòu)
點贊
收藏

51CTO技術(shù)棧公眾號