<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                # go tool pprof 我們可以使用```go tool pprof```命令來交互式的訪問概要文件的內容。命令將會分析指定的概要文件,并會根據我們的要求為我們提供高可讀性的輸出信息。 在Go語言中,我們可以通過標準庫的代碼包```runtime```和```runtime/pprof```中的程序來生成三種包含實時性數據的概要文件,分別是CPU概要文件、內存概要文件和程序阻塞概要文件。下面我們先來分別介紹用于生成這三種概要文件的API的用法。 **CPU概要文件** 在介紹CPU概要文件的生成方法之前,我們先來簡單了解一下CPU主頻。CPU的主頻,即CPU內核工作的時鐘頻率(CPU Clock Speed)。CPU的主頻的基本單位是赫茲(Hz),但更多的是以兆赫茲(MHz)或吉赫茲(GHz)為單位。時鐘頻率的倒數即為時鐘周期。時鐘周期的基本單位為秒(s),但更多的是以毫秒(ms)、微妙(us)或納秒(ns)為單位。在一個時鐘周期內,CPU執行一條運算指令。也就是說,在1000 Hz的CPU主頻下,每1毫秒可以執行一條CPU運算指令。在1 MHz的CPU主頻下,每1微妙可以執行一條CPU運算指令。而在1 GHz的CPU主頻下,每1納秒可以執行一條CPU運算指令。 在默認情況下,Go語言的運行時系統會以100 Hz的的頻率對CPU使用情況進行取樣。也就是說每秒取樣100次,即每10毫秒會取樣一次。為什么使用這個頻率呢?因為100 Hz既足夠產生有用的數據,又不至于讓系統產生停頓。并且100這個數上也很容易做換算,比如把總取樣計數換算為每秒的取樣數。實際上,這里所說的對CPU使用情況的取樣就是對當前的Goroutine的堆棧上的程序計數器的取樣。由此,我們就可以從樣本記錄中分析出哪些代碼是計算時間最長或者說最耗CPU資源的部分了。我們可以通過以下代碼啟動對CPU使用情況的記錄。 func startCPUProfile() { if *cpuProfile != "" { f, err := os.Create(*cpuProfile) if err != nil { fmt.Fprintf(os.Stderr, "Can not create cpu profile output file: %s", err) return } if err := pprof.StartCPUProfile(f); err != nil { fmt.Fprintf(os.Stderr, "Can not start cpu profile: %s", err) f.Close() return } } } 在函數```startCPUProfile```中,我們首先創建了一個用于存放CPU使用情況記錄的文件。這個文件就是CPU概要文件,其絕對路徑由```*cpuProfile```的值表示。然后,我們把這個文件的實例作為參數傳入到函數```pprof.StartCPUProfile``中。如果此函數沒有返回錯誤,就說明記錄操作已經開始。需要注意的是,只有CPU概要文件的絕對路徑有效時此函數才會開啟記錄操作。 如果我們想要在某一時刻停止CPU使用情況記錄操作,就需要調用下面這個函數: func stopCPUProfile() { if *cpuProfile != "" { pprof.StopCPUProfile() // 把記錄的概要信息寫到已指定的文件 } } 在這個函數中,并沒有代碼用于CPU概要文件寫入操作。實際上,在啟動CPU使用情況記錄操作之后,運行時系統就會以每秒100次的頻率將取樣數據寫入到CPU概要文件中。```pprof.StopCPUProfile```函數通過把CPU使用情況取樣的頻率設置為0來停止取樣操作。并且,只有當所有CPU使用情況記錄都被寫入到CPU概要文件之后,```pprof.StopCPUProfile```函數才會退出。從而保證了CPU概要文件的完整性。 **內存概要文件** 內存概要文件用于保存在用戶程序執行期間的內存使用情況。這里所說的內存使用情況,其實就是程序運行過程中堆內存的分配情況。Go語言運行時系統會對用戶程序運行期間的所有的堆內存分配進行記錄。不論在取樣的那一時刻、堆內存已用字節數是否有增長,只要有字節被分配且數量足夠,分析器就會對其進行取樣。開啟內存使用情況記錄的方式如下: func startMemProfile() { if *memProfile != "" && *memProfileRate > 0 { runtime.MemProfileRate = *memProfileRate } } 我們可以看到,開啟內存使用情況記錄的方式非常簡單。在函數```startMemProfile```中,只有在```*memProfile```和```*memProfileRate```的值有效時才會進行后續操作。```*memProfile```的含義是內存概要文件的絕對路徑。```*memProfileRate```的含義是分析器的取樣間隔,單位是字節。當我們將這個值賦給int類型的變量```runtime.MemProfileRate```時,就意味著分析器將會在每分配指定的字節數量后對內存使用情況進行取樣。實際上,即使我們不給```runtime.MemProfileRate```變量賦值,內存使用情況的取樣操作也會照樣進行。此取樣操作會從用戶程序開始時啟動,且一直持續進行到用戶程序結束。```runtime.MemProfileRate```變量的默認值是```512 * 1024```,即512K個字節。只有當我們顯式的將```0```賦給```runtime.MemProfileRate```變量之后,才會取消取樣操作。 在默認情況下,內存使用情況的取樣數據只會被保存在運行時內存中,而保存到文件的操作只能由我們自己來完成。請看如下代碼: func stopMemProfile() { if *memProfile != "" { f, err := os.Create(*memProfile) if err != nil { fmt.Fprintf(os.Stderr, "Can not create mem profile output file: %s", err) return } if err = pprof.WriteHeapProfile(f); err != nil { fmt.Fprintf(os.Stderr, "Can not write %s: %s", *memProfile, err) } f.Close() } } 從函數名稱上看,```stopMemProfile```函數的功能是停止對內存使用情況的取樣操作。但是,它只做了將取樣數據保存到內存概要文件的操作。在```stopMemProfile```函數中,我們調用了函數```pprof.WriteHeapProfile```,并把代表內存概要文件的文件實例作為了參數。如果```pprof.WriteHeapProfile```函數沒有返回錯誤,就說明數據已被寫入到了內存概要文件中。 需要注意的是,對內存使用情況進行取樣的程序會假定取樣間隔在用戶程序的運行期間內都是一成不變的,并且等于```runtime.MemProfileRate```變量的當前值。因此,我們應該在我們的程序中只改變內存取樣間隔一次,且應盡早改變。比如,在命令源碼文件的main函數的開始處就改變它。 **程序阻塞概要文件** 程序阻塞概要文件用于保存用戶程序中的Goroutine阻塞事件的記錄。我們來看開啟這項操作的方法: func startBlockProfile() { if *blockProfile != "" && *blockProfileRate > 0 { runtime.SetBlockProfileRate(*blockProfileRate) } } 與開啟內存使用情況記錄的方式類似,在函數```startBlockProfile```中,當```*blockProfile```和```*blockProfileRate```的值有效時,我們會設置對Goroutine阻塞事件的取樣間隔。```*blockProfile```的含義為程序阻塞概要文件的絕對路徑。```*blockProfileRate```的含義是分析器的取樣間隔,單位是次。函數```runtime.SetBlockProfileRate```的唯一參數是int類型的。它的含義是分析器會在每發生幾次Goroutine阻塞事件時對這些事件進行取樣。如果我們不顯式的使用```runtime.SetBlockProfileRate```函數設置取樣間隔,那么取樣間隔就為1。也就是說,在默認情況下,每發生一次Goroutine阻塞事件,分析器就會取樣一次。與內存使用情況記錄一樣,運行時系統對Goroutine阻塞事件的取樣操作也會貫穿于用戶程序的整個運行期。但是,如果我們通過```runtime.SetBlockProfileRate```函數將這個取樣間隔設置為```0```或者負數,那么這個取樣操作就會被取消。 我們在程序結束之前可以將被保存在運行時內存中的Goroutine阻塞事件記錄存放到指定的文件中。代碼如下: func stopBlockProfile() { if *blockProfile != "" && *blockProfileRate >= 0 { f, err := os.Create(*blockProfile) if err != nil { fmt.Fprintf(os.Stderr, "Can not create block profile output file: %s", err) return } if err = pprof.Lookup("block").WriteTo(f, 0); err != nil { fmt.Fprintf(os.Stderr, "Can not write %s: %s", *blockProfile, err) } f.Close() } } 在創建程序阻塞概要文件之后,```stopBlockProfile```函數會先通過函數```pprof.Lookup```將保存在運行時內存中的內存使用情況記錄取出,并在記錄的實例上調用```WriteTo```方法將記錄寫入到文件中。 **更多的概要文件** 我們可以通過```pprof.Lookup```函數取出更多種類的取樣記錄。如下表: _表0-20 可從pprof.Lookup函數中取出的記錄_ <table> <tr> <th width=60px> 名稱 </th> <th width=180px> 說明 </th> <th width=100px> 取樣頻率 </th> </tr> <tr> <td> goroutine </td> <td> 活躍Goroutine的信息的記錄。 </td> <td> 僅在獲取時取樣一次。 </td> </tr> <tr> <td> threadcreate </td> <td> 系統線程創建情況的記錄。 </td> <td> 僅在獲取時取樣一次。 </td> </tr> <tr> <td> heap </td> <td> 堆內存分配情況的記錄。 </td> <td> 默認每分配512K字節時取樣一次。 </td> </tr> <tr> <td> block </td> <td> Goroutine阻塞事件的記錄。 </td> <td> 默認每發生一次阻塞事件時取樣一次。 </td> </tr> </table> 在上表中,前兩種記錄均為一次取樣的記錄,具有即時性。而后兩種記錄均為多次取樣的記錄,具有實時性。實際上,后兩種記錄“heap”和“block”正是我們前面講到的內存使用情況記錄和程序阻塞情況記錄。 我們知道,在用戶程序運行期間各種狀態是在不斷變化的。尤其對于后兩種記錄來說,隨著取樣次數的增多,記錄項的數量也會不斷增長。而對于前兩種記錄“goroutine”和“threadcreate”來說,如果有新的活躍Goroutine產生或新的系統線程被創建,其記錄項數量也會增大。所以,Go語言的運行時系統在從內存中獲取記錄時都會先預估一個記錄項數量。如果在從預估記錄項數量到獲取記錄之間的時間里又有新記錄項產生,那么運行時系統會試圖重新獲取全部記錄項。另外,運行時系統使用切片來裝載所有記錄項的。如果當前使用的切片裝不下所有記錄項,運行時系統會根據當前記錄項總數創建一個更大的切片,并再次試圖裝載所有記錄項。直到這個切片足以裝載所有的記錄項為止。但是,如果記錄項增長過快的話,運行時系統將不得不不斷的進行嘗試。這可能導致過多的時間消耗。對于前兩種記錄“goroutine”和“threadcreate”來說,運行時系統創建的切片的大小為當前記錄項總數再加10。對于前兩種記錄“heap”和“block”來說,運行時系統創建的切片的大小為當前記錄項總數再加50。雖然上述情況發生的概率可能并不會太高,但是如果我們在對某些高并發的用戶程序獲取上述記錄的時候耗費的時間過長,可以先排查一下這類原因。實際上,我們在前面介紹的這幾 種記錄操作更適合用于對高并發的用戶程序進行狀態檢測。 我們可以通過下面這個函數分別將四種記錄輸出到文件。 func SaveProfile(workDir string, profileName string, ptype ProfileType, debug int) { absWorkDir := getAbsFilePath(workDir) if profileName == "" { profileName = string(ptype) } profilePath := filepath.Join(absWorkDir, profileName) f, err := os.Create(profilePath) if err != nil { fmt.Fprintf(os.Stderr, "Can not create profile output file: %s", err) return } if err = pprof.Lookup(string(ptype)).WriteTo(f, debug); err != nil { fmt.Fprintf(os.Stderr, "Can not write %s: %s", profilePath, err) } f.Close() } 函數```SaveProfile```有四個參數。第一個參數是概要文件的存放目錄。第二個參數是概要文件的名稱。第三個參數是概要文件的類型。其中,類型```ProfileType```只是為string類型起的一個別名而已。這樣是為了對它的值進行限制。它的值必須為“goroutine”、“threadcreate”、“heap”或“block”中的一個。我們現在來重點說一下第四個參數。參數```debug```控制著概要文件中信息的詳細程度。這個參數也就是傳給結構體```pprof.Profile```的指針方法```WriteTo```的第二個參數。而```pprof.Profile```結構體的實例的指針由函數```pprof.Lookup```產生。下面我們看看參數debug的值與寫入概要文件的記錄項內容的關系。 _表0-21 參數debug對概要文件內容的影響_ <table class="table table-bordered table-striped table-condensed"> <tr> <th width=100px> 記錄\debug </th> <th width=120px> 小于等于0 </th> <th width=120px> 等于1 </th> <th width=120px> 大于等于2 </th> </tr> <tr> <td> goroutine </td> <td> 為每個記錄項提供調用棧中各項的以十六進制表示的內存地址。 </td> <td> 在左邊提供的信息的基礎上,為每個記錄項的調用棧中各項提供與內存地址對應的帶代碼包導入路徑的函數名和源碼文件路徑及源碼所在行號。 </td> <td> 以高可讀的方式提供各活躍Goroutine的狀態信息和調用棧信息。 </td> </tr> <tr> <td> threadcreate </td> <td> 同上。 </td> <td> 同上。 </td> <td> 同左。 </td> </tr> <tr> <td> heap </td> <td> 同上。 </td> <td> 在左邊提供的信息的基礎上,為每個記錄項的調用棧中各項提供與內存地址對應的帶代碼包導入路徑的函數名和源碼文件路徑及源碼所在行,并提供內存狀態信息。 </td> <td> 同左。 </td> </tr> <tr> <td> block </td> <td> 同上。 </td> <td> 在左邊提供的信息的基礎上,為每個記錄項的調用棧中各項提供與內存地址對應的帶代碼包導入路徑的函數名和源碼文件路徑及源碼所在行號。 </td> <td> 同左。 </td> </tr> </table> 從上表可知,當```debug```的值小于等于```0```時,運行時系統僅會將每個記錄項中的基本信息寫入到概要文件中。記錄項的基本信息只包括其調用棧中各項的以十六進制表示的內存地址。```debug```的值越大,我們能從概要文件中獲取的信息越多。但是,```go tool pprof```命令會無視那些除基本信息以外的附加信息。實際上,運行時系統在向概要文件中寫入附加信息時會在最左邊加入“#”,以表示當前行為注釋行。也正因為有了這個前綴,```go tool pprof```命令才會略過對這些附加信息的解析。這其中有一個例外,那就是當```debug```大于等于```2```時,Goroutine記錄并不是在基本信息的基礎上附加信息,而是完全以高可讀的方式寫入各活躍Goroutine的狀態信息和調用棧信息。并且,在所有行的最左邊都沒有前綴“#”。顯然,這個概要文件是無法被```go tool pprof```命令解析的。但是它對于我們來說會更加直觀和有用。 至此,我們已經介紹了使用標準庫代碼包```runtime```和```runtime/pprof```中的程序生成概要文件的全部方法。在上面示例中的所有代碼都被保存在goc2p項目的代碼包```basic/prof```中。代碼包```basic/prof```中的這些程序非常易于使用。不過由于Go語言目前沒有類似停機鉤子(Shutdown Hook)的API(應用程序接口),所以代碼包```basic/prof```中的程序目前只能以侵入式的方式被使用。 **pprof工具** 我們在上一小節中提到過,任何以```go tool```開頭的Go命令內部指向的特殊工具都被保存在目錄$GOROOT/pkg/tool/$GOOS_$GOARCH/中。我們把這個目錄叫做Go工具目錄。與其他特殊工具不同的是,pprof工具并不是用Go語言編寫的,而是由Perl語言編寫的。(Perl是一種通用的、動態的解釋型編程語言)與Go語言不同,Perl語言可以直接讀取源碼并運行。正因為如此,```pprof```工具的源碼文件被直接保存在了Go工具目錄下。而對于其它Go工具,存在此目錄的都是經過編譯而生成的可執行文件。我們可以直接用任意一種文本查看工具打開在Go工具目錄下的pprof工具的源碼文件pprof。實際上,這個源碼文件拷貝自Google公司發起的開源項目gperftools。此項目中包含了很多有用的工具。這些工具可以幫助開發者創建更健壯的應用程序。pprof就是其中的一個非常有用的工具。 因為```pprof```工具是用Perl語言編寫的,所以執行```go tool pprof```命令的前提條件是需要在當前環境下安裝Perl語言,推薦的版本號是5.x。關于Perl語言的安裝方法就不在這里敘述了,讀者可以自己找到方法并自行安裝它。在安裝完Perl語言之后,我們可以在命令行終端中進入到Go工具目錄,并執行命令```perl pprof```。它與我們在任意目錄下執行```go tool pprof```命令的效果是一樣的。當然,如果想要讓```go tool pprof```命令在任意目錄下都可以被執行,我們需要先設置好與Go語言相關的環境變量。 我們在本小節已經討論過,```go tool pprof```命令會分析指定的概要文件并使得我們能夠以交互式的方式訪問其中的信息。但是光有概要文件還不夠,我們還需要概要文件中信息的來源——命令源碼文件的可執行文件。畢竟,概要文件中的信息是對在運行期間的用戶程序取樣的結果。而可以運行的Go語言程序只能是編譯命令源碼文件后生成的可執行文件。因此,為了演示```go tool pprof```命令的用法,我們還創建或改造一個命令源碼文件。在我們的goc2p項目的代碼包中有一個名稱為showpds.go的命令源碼文件。這個命令源碼文件用來解析指定的代碼包的依賴關系,并將這些依賴關系打印到標準輸出設備上。選用這個命令源碼文件的原因是,我們可以通過改變指定的代碼包來控制這個命令源碼文件的運行時間的長短。不同的代碼包可能會有不同數量的直接依賴包和間接依賴包。依賴包越多的代碼包會使這個命令源碼文件耗費更多的時間來解析它的依賴關系。命令源碼文件運行的時間越長,我們得到的概要文件中的信息就越有意義。為了生成概要文件,我們需要稍微的改造一下這個命令源碼文件。首先我們需要在這個文件中導入代碼包```basic/prof```。然后,我們需要在它的main函數的開頭加入一行代碼```prof.Start()```。這行代碼的含義是檢查相關標記,并在標記有效時開啟或設置對應的使用情況記錄操作。最后,我們還需要在main函數的defer代碼塊中加入一行代碼```prof.Stop()```。這行代碼的含義是,獲取已開啟的記錄的取樣數據并將它們寫入到指定的概要文件中。通過這三個步驟,我們就已經把生成運行時概要文件的功能附加到這個命令源碼文件中了。為了開啟這些功能,我還需要在通過執行```go run```命令來運行這個命令源碼文件的時候,加入相應的標記。對代碼包```basic/prof```中的程序有效的標記如下表。 _表0-22 對代碼包basic/prof的API有效的標記_ <table class="table table-bordered table-striped table-condensed"> <tr> <th width=125px> 標記名稱 </th> <th> 標記描述 </th> </tr> <tr> <td> -cpuprofile </td> <td> 指定CPU概要文件的保存路徑。該路徑可以是相對路徑也可以是絕對路徑,但其父路徑必須已存在。 </td> </tr> <tr> <td> -blockprofile </td> <td> 指定程序阻塞概要文件的保存路徑。該路徑可以是相對路徑也可以是絕對路徑,但其父路徑必須已存在。 </td> </tr> <tr> <td> -blockprofilerate </td> <td> 定義其值為n。此標記指定每發生n次Goroutine阻塞事件時,進行一次取樣操作。 </td> </tr> <tr> <td> -memprofile </td> <td> 指定內存概要文件的保存路徑。該路徑可以是相對路徑也可以是絕對路徑,但其父路徑必須已存在。 </td> </tr> <tr> <td> -memprofilerate </td> <td> 定義其值為n。此標記指定每分配n個字節的堆內存時,進行一次取樣操作。 </td> </tr> </table> 下面我們使用```go run```命令運行改造后的命令源碼文件showpds.go。示例如下: hc@ubt:~/golang/goc2p$ mkdir pprof hc@ubt:~/golang/goc2p$ cd helper/pds hc@ubt:~/golang/goc2p/helper/pds$ go run showpds.go -p="runtime" cpuprofile="../../../pprof/cpu.out" -blockprofile="../../../pprof/block.out" -blockprofilerate=1 -memprofile="../../../pprof/mem.out" -memprofilerate=10 The package node of 'runtime': {/usr/local/go/src/pkg/ runtime [] [] false} The dependency structure of package 'runtime': runtime->unsafe 在上面的示例中,我們使用了所有的對代碼包```basic/prof```的API有效的標記。另外,標記```-p```是對命令源碼文件showpds.go有效的。其含義是指定要解析依賴關系的代碼包的導入路徑。 現在我們來查看一下goc2p項目目錄下的pprof子目錄: hc@ubt:~/golang/goc2p/helper/pds$ ls ../../../pprof block.out cpu.out mem.out 這個目錄中的三個文件分別對應了三種包含實時性數據的概要文件。這也證明了我們對命令源碼文件showpds.go的改造是有效的。 好了,一切準備工作就緒。現在,我們就來看看```go tool pprof```命令都能做什么。首先,我們來編譯命令源碼文件showpds.go。 hc@ubt:~/golang/goc2p/helper/pds$ go build showpds.go hc@ubt:~/golang/goc2p/helper/pds$ ls showpds showpds.go 然后,我們需要準備概要文件。標準庫代碼包```runtime```的依賴包極少,這使得可執行文件showpds在極短的時間內就會運行完畢。之前我們說過,程序運行的時間越長越好。所以我們需要找到一個直接和間接依賴包都很多的代碼包。做過Web應用系統開發的同行們都知道,一個Web應用系統的后端程序可能會有很多的依賴,不論是代碼庫還是其他資源。根據我們的直覺,在Go語言的世界里也應該是在這樣。在Go語言的標準庫中,代碼包```net/http```專門用來為Web應用系統開發提供各種API支持。我們就用這個代碼包來生成所需的概要文件。 hc@ubt:~/golang/goc2p/helper/pds$ ./showpds -p="net/http" -cpuprofile="../../../pprof/cpu.out" -blockprofile="../../../pprof/block.out" -blockprofilerate=1 -memprofile="../../../pprof/mem.out" -memprofilerate=10 標準庫代碼包```net/http```的依賴包很多。也正因為如此,我忽略了所有輸出的內容。讀者可以自己試試上面的這個命令。我們一口氣生成了所有能夠生成的概要文件作為備用。這寫概要文件被保存在了goc2p項目的pprof目錄中。如果在上面的命令被執行前還沒有pprof目錄,命令會報錯。所以讀者需要先創建這個目錄。 現在我們就以可執行文件showpds和pprof目錄下的CPU概要文件cpu.out作為參數來執行```go tool pprof```命令。實際上,我們通過```go tool pprof```命令進入的就是pprof工具的交互模式的界面。 hc@ubt:~/golang/goc2p/helper/pds$ go tool pprof showpds ../../../pprof/cpu.out Welcome to pprof! For help, type 'help'. (pprof) 我們可以在提示符“(pprof)”后面輸入一些命令來查看概要文件。pprof工具在交互模式下支持的命令如下表。 _表0-23 pprof工具在交互模式下支持的命令_ <table class="table table-bordered table-striped table-condensed"> <tr> <th width=40px> 名稱 </th> <th width=80px> 參數 </th> <th width=50px> 標簽 </th> <th> 說明 </th> </tr> <tr> <td> gv </td> <td> [focus] </td> <td> </td> <td> 將當前概要文件以圖形化和層次化的形式顯示出來。當沒有任何參數時,在概要文件中的所有抽樣都會被顯示。如果指定了focus參數,則只顯示調用棧中有名稱與此參數相匹配的函數或方法的抽樣。focus參數應該是一個正則表達式。 </td> </tr> <tr> <td> web </td> <td> [focus] </td> <td> </td> <td> 與gv命令類似,web命令也會用圖形化的方式來顯示概要文件。但不同的是,web命令是在一個Web瀏覽器中顯示它。如果你的Web瀏覽器已經啟動,那么它的顯示速度會非常快。如果想改變所使用的Web瀏覽器,可以在Linux下設置符號鏈接/etc/alternatives/gnome-www-browser或/etc/alternatives/x-www-browser,或在OS X下改變SVG文件的關聯Finder。 </td> </tr> <tr> <td> list </td> <td> [routine_regexp] </td> <td> </td> <td> 列出名稱與參數“routine_regexp”代表的正則表達式相匹配的函數或方法的相關源代碼。 </td> </tr> <tr> <td> weblist </td> <td> [routine_regexp] </td> <td> </td> <td> 在Web瀏覽器中顯示與list命令的輸出相同的內容。它與list命令相比的優勢是,在我們點擊某行源碼時還可以顯示相應的匯編代碼。 </td> </tr> <tr> <td> top[N] </td> <td> </td> <td> [--cum] </td> <td> top命令可以以本地取樣計數為順序列出函數或方法及相關信息。如果存在標記“--cum”則以累積取樣計數為順序。默認情況下top命令會列出前10項內容。但是如果在top命令后面緊跟一個數字,那么其列出的項數就會與這個數字相同。 </td> </tr> <tr> <td> disasm </td> <td> [routine_regexp] </td> <td> </td> <td> 顯示名稱與參數“routine_regexp”相匹配的函數或方法的反匯編代碼。并且,在顯示的內容中還會標注有相應的取樣計數。 </td> </tr> <tr> <td> callgrind </td> <td> [filename] </td> <td> </td> <td> 利用callgrind工具生成統計文件。在這個文件中,說明了程序中函數的調用情況。如果未指定“filename”參數,則直接調用kcachegrind工具。kcachegrind可以以可視化的方式查看callgrind工具生成的統計文件。 </td> </tr> <tr> <td> help </td> <td> </td> <td> </td> <td> 顯示幫助信息。 </td> </tr> <tr> <td> quit </td> <td> </td> <td> </td> <td> 退出go tool pprof命令。Ctrl-d也可以達到同樣效果。 </td> </tr> </table> 在上面表格中的絕大多數命令(除了help和quit)都可以在其所有參數和標簽后追加一個或多個參數,以表示想要忽略顯示的函數或方法的名稱。我們需要在此類參數上加入減號“-”作為前綴,并且多個參數之間需要以空格分隔。當然,我們也可以用正則表達式替代函數或方法的名稱。追加這些約束之后,任何調用棧中包含名稱與這類參數相匹配的函數或方法的抽樣都不會出現在命令的輸出內容中。下面我們對這幾個命令進行逐一說明。 **gv命令** 對于命令gv的用法,請看如下示例: hc@ubt:~/golang/goc2p/helper/pds$ go tool pprof showpds ../../../pprof/cpu.out Welcome to pprof! For help, type 'help'. (pprof) gv Total: 101 samples sh: 1: dot: not found go tool pprof: signal: broken pipe 其中,“(pprof)”是pprof工具在交互模式下的提示符。 從輸出信息中我們可以看到,gv命令并沒有正確的被執行。原因是沒有找到命令dot。經查,這個命令屬于一個開源軟件Graphviz。Graphviz的核心功能是圖表的可視化。我們可以通過命令```sudo apt-get install graphviz```來安裝這個軟件。注意,上面這條命令僅可用于Debian的Linux發行版及其衍生版。如果是在Redhat的Linux發行版及其衍生版下,可以使用命令“yum install graphviz”來安裝Graphviz。安裝好Graphviz后,我們再來執行gv命令。 (pprof) gv Total: 101 samples gv -scale 0 (pprof) sh: 1: gv: not found 現在,輸出信息有提示我們沒有找到命令gv。gv是自由軟件工程項目GNU(GNU's Not Unix)中的一款開源軟件,用來以圖形化的方式查看PDF文檔。我們以同樣的方式安裝它。在Debian的Linux發行版及其衍生版下,執行命令```sudo apt-get install gv```,在Redhat的Linux發行版及其衍生版下,執行命令```yum install gv```。軟件gv被安裝好后,我們再次執行gv命令。在運行著圖形界面軟件的Linux操作系統下,會彈出這樣一個窗口。如圖5-3。 ![pprof工具的gv命令的執行結果](https://box.kancloud.cn/6ba52f568970cfe69d252a6ac65c452c_1298x633.jpg) _圖0-3 pprof工具的gv命令的執行結果_ 我們看到,在概要圖的最上面顯示了一些基本的信息。其中,“showpds”是我們生成概要文件時用到的那個可執行文件。它也是概要文件中內容的來源。“Total samples:”后面的數字23的含義是在本次程序執行期間分析器一共進行了23次取樣。我們已經知道,CPU使用情況的取樣操作會以每10毫秒一次的頻率進行。因此,取樣23次就意味著程序運行所花費的CPU時間大約為```10毫秒 * 23 = 0.23秒```。由于我們并沒有在gv命令后加入用于約束顯示內容的參數focus,所以在“Focusing on:”后面的數字也是23。也正是由于這個原因,后邊兩行信息中的數字均為0。讀者可以自行試驗一下在gv命令后加入focus參數的情形,例如:```gv ShowDepStruct```。在下面的描述中,我們把函數和方法統稱為函數。 現在,我們把視線放在主要的圖形上。此圖形由矩形和有向線段組成。在此圖形的大多數矩形中都包含三行信息。第一行是函數的名字。第二行包含了該函數的本地取樣計數(在括號左邊的數字)及其在取樣總數中所占的比例(在括號內的百分比)。第三行則包含了該函數的累積取樣計數(括號左邊的數字)及其在取樣總數中所占的比例(在括號內的百分比)。 首先,讀者需要搞清楚兩個相似但不相同的概念,即:本地取樣計數和累積取樣計數。本地取樣計數的含義是當前函數在取樣中直接出現的次數。累積取樣計數的含義是當前函數以及當前函數直接或間接調用的函數在取樣中直接出現的次數。所以,存在這樣一種場景:對于一個函數來說,它的本地取樣計數是0。因為它沒有在取樣中直接出現過。但是,由于它直接或間接調用的函數頻繁的直接出現在取樣中,所以這個函數的累積取樣計數卻會很高。我們以上圖中的函數mian.main為例。由于main.main函數在所有取樣中都沒有直接出現過,所以它的本地取樣計數為0。但又由于它是命令源碼文件中的入口函數,程序中其他的函數都直接或間接的被它調用。所以,它的累積取樣計數是所有函數中最高的,達到了22。注意,不論是本地取樣計數還是累積取樣計數都沒有把函數對自身的調用計算在內。函數對自身的調用又被稱為遞歸調用。 最后需要說明的是,圖形中的有向線段表示函數之間的調用關系。有向線段旁邊的數字為線段起始位置的函數對線段末端位置的函數的調用計數。這里所說的調用計數其實是以函數的累積取樣計數為依托的。更具體的講,如果有一個從函數A到函數B的有向線段且旁邊的數字為10,那么就說明在函數B的累加取樣計數中有10次計數是由函數A對函數B的直接調用所引起的。也由于這個原因,函數A對函數B的調用計數必定小于等于函數B的累積取樣計數。 至此,我們已經對概要圖中的所有元素都進行了說明,相信讀者已經能夠讀懂它了。那么,我們怎樣通過概要圖對程序進行分析呢? 我們可以把概要圖中的這個主要圖形看成是一張函數調用關系圖。在一般情況下,處在非終端節點位置的函數的本地取樣計數會非常小,至少會比該函數的累積取樣計數小很多。因為它們都是通過對其它函數的調用來實現自身的功能的。進一步說,所有使用Go語言編寫的代碼的功能最后都需要依托操作系統所提供的API來實現。處在終端節點位置的函數一般都存在于平臺相關的源碼文件中,甚至有的函數本身就是操作系統的某個API在Go語言中的映射。它們的累積取樣計數與本地取樣計數是一致的。因此,這類函數的描述信息只有兩行,即它的名稱和它的累積取樣計數。 現在我們已經明確了在概要圖中出現的一個函數的本地取樣計數、累積取樣計數和調用計數的概念和含義以及它們之間的關系。這三個計數是我們分析程序性能的重要依據。 我們可以通過一個函數的累積取樣次數計算出執行它所花費的時間。一個函數的累積取樣計數越大就說明調用它所花費的CPU時間越多。具體來說,我們可以用CPU取樣間隔(10毫秒)乘以函數的累積取樣計數得出它所花費的實際時間。雖然這個實際時間只精確到了10毫秒的級別,但是這對于程序性能分析來說已經足夠了。即使一個函數的累積取樣計數很大,我們也不能判定這個函數本身就是有問題的。我們應該順藤摸瓜,去尋找這個函數直接或間接調用的函數中最耗費CPU時間的那些函數。其實,這樣的查找很容易,因為我們已經有了概要圖。在其中的函數調用關系圖中,累積取樣計數越大的函數就擁有更大的節點(圖中的矩形)面積。不過這也有例外,那就是程序的入口函數。廣義來講,在整個函數調用關系中處在初始位置附近且與之相連的有向線段在同一方向上至多只有一個的函數都可以被稱作入口函數。無論它們的累積取樣計數有多大,其所屬的節點的面積都是在函數調用關系圖中最小的。由于出現在取樣和函數調用關系圖中的所有函數幾乎都源自入口函數的直接或間接的調用,所以入口函數的累積取樣次數必定是它們中最大的。一般情況下,我們并不需要在意入口函數的計數數值,所以在函數調用關系圖中也就不需要使用大面積的節點來強調它們。在圖5-3中,函數```runtime.main```和```main.main```都可以被視為入口函數。另外,在函數調用關系圖中,有向線段的粗細也反應了對應的調用計數的大小。 下面,作者總結了根據函數的相關計數來對其進行分析的三個過程: 1. 如果一個處在終端節點位置上的函數的累積取樣計數和百分比都很大,就說明它自身花費了過多的CPU時間。這時,需要檢查這個函數所實現的功能是否確實需要花費如此多的時間。如果花費的時間超出了我們的估算,則需要通過list命令找出函數體內最耗時的代碼并進行進一步分析。如果我們發現這個函數所承擔的職責過多,那么可以直接將這個函數拆分成多個擁有不同職責的更小的函數。 2. 如果一個處在非終端節點位置上的函數的累積取樣計數和百分比都很大并且超出了我們的估算,那么我們應該首先查看其本地取樣計數的大小。如果它的本地取樣計數和百分比也很大,我們就需要通過list命令對這個函數體中的代碼進行進一步分析。否則,我們就應該把關注點放在其下的分支節點所對應的函數上。如果當前節點下的所有直接分支節點的函數的累積取樣計數都不大,但是直接分支節點的數量卻非常多(十幾甚至幾十個),那么大致上可以斷定當前節點的函數承擔了過多的與流程控制相關的職責,我們需要對它進行拆分甚至重新設計。如果當前節點下的分支節點中包含累積取樣計數和百分比很大的函數,那么我們就應該根據這個分支節點的類型(終端節點或非終端節點)來對其進行過程1或過程2的分析。 3. 單從調用計數的角度,我們并不能判斷一個函數是否承擔了過多的職責或者包含了過多的流程控制邏輯。但是,我們可以把調用計數作為定位問題的一種輔助手段。舉個例子,如果根據過程1和過程2中的分析,我們懷疑在函數```B```及其調用的函數中可能存在性能問題,并且我們還發現函數```A```對函數```B```的調用計數也非常大,那么我們就應該想到函數```B```在取樣中的頻繁出現也許是由函數```A```對函數```B```的頻繁調用引起的。在這種情況下,我們就應該先查看函數```A```中的代碼,檢查其中是否包含了過多的對函數```B```的不合理調用。如果存在不合理的調用,我們就應該對這部分代碼進行重新設計。除此之外,我們還可以根據調用計數來判定一些小問題。比如,如果一個函數與調用它的所有函數都處于同一個代碼包,那么我們就應該考慮把被調用的函數的訪問權限設置為包內私有。如果對一個函數的調用都是來自于同一個函數,我們可以考慮在符合單一職責原則的情況下把這兩個函數合并。讀者可能已經注意到,這與過程1中的一個建議是相互對立的。實際上,這也屬于一種推遲優化策略。 在上述幾個分析過程中的所有建議都不是絕對的。程序優化是一個復雜的過程,在很多時候都需要在多個指標或多個解決方案之間進行權衡和博弈。 在這幾個分析過程的描述中,我們多次提到了list命令。現在我們就來對它進行說明。先來看一個示例: (pprof) list ShowDepStruct Total: 23 samples ROUTINE ====================== main.ShowDepStruct in /home/hc/golang/goc2p /src/helper/pds/showpds.go 0 20 Total samples (flat / cumulative) . . 44: } . . 45: fmt.Printf("The dependency structure of package '%s':\n", pkgImportPath) . . 46: ShowDepStruct(pn, "") . . 47: } . . 48: --- . . 49: func ShowDepStruct(pnode *pkgtool.PkgNode, prefix string) { . . 50: var buf bytes.Buffer . . 51: buf.WriteString(prefix) . . 52: importPath := pnode.ImportPath() . 2 53: buf.WriteString(importPath) . 1 54: deps := pnode.Deps() . . 55: //fmt.Printf("P_NODE: '%s', DEP_LEN: %d\n", importPath, len(deps)) . . 56: if len(deps) == 0 { . 5 57: fmt.Printf("%s\n", buf.String()) . . 58: return . . 59: } . . 60: buf.WriteString(ARROWS) . . 61: for _, v := range deps { . 12 62: ShowDepStruct(v, buf.String()) . . 63: } . . 64: } --- . . 65: . . 66: func getPkgImportPath() string { . . 67: if len(pkgImportPathFlag) > 0 { . . 68: return pkgImportPathFlag . . 69: } (pprof) 我們在pprof工具的交互界面中輸入了命令```list ShowDepStruct```之后得到了很多輸出信息。其中,ShowDepStruct為參數routine_regexp的值。輸出信息的第一行告訴我們CPU概要文件中的取樣一共有23個。這與我們之前講解gv命令時看到的一樣。輸出信息的第二行顯示,與我們提供的程序正則表達式(也就是參數routine_regexp)的值匹配的函數是```main.ShowDepStruct```,并且這個函數所在的源碼文件的絕對路徑是/home/hc/golang/goc2p/src/helper/pds/showpds.go。輸出信息中的第三行告訴我們,在```main.ShowDepStruct```函數體中的代碼的本地取樣計數的總和是0,而累積取樣計數的總和是20。在第三行最右邊的括號中,flat代表本地取樣計數,而cumulative代表累積取樣計數。這是對該行最左邊的那兩個數字(也就是0和20)的含義的提示。從輸出信息的第四行開始是對上述源碼文件中的代碼的截取,其中包含了```main.ShowDepStruct```函數的源碼。list命令在這些代碼的左邊添加了對應的行號,這讓我們查找代碼更加容易。另外,在代碼行號左邊的對應位置上顯示了每行代碼的本地取樣計數和累積取樣計數。如果計數為0,則用英文句號“.”代替。這使得我們可以非常方便的找到存在計數值的代碼行。 一般情況下,每行代碼對應的本地取樣計數和累積取樣計數都應該與我們用gv命令生成的函數調用關系圖中的計數相同。但是,如果一行代碼中存在多個函數調用的話,那么在代碼行號左邊的計數值就會有偏差。比如,在上述示例中,第62行代碼```ShowDepStruct(v, buf.String())```的累積取樣計數是12。但是從之前的函數調用關系圖中我們得知,函數```main.ShowDepStruct```的累積取樣計數是10。它們之間的偏差是2。實際上,在程序被執行的時候,第62行代碼是由兩個操作步驟組成的。第一個步驟是執行函數調用```buf.String()```并獲得結果。第二個步驟是,調用函數```ShowDepStruct```,同時將變量```v``和執行第一個步驟所獲得的結果作為參數傳入。所以,這2個取樣計數應該歸咎于第62行代碼中的函數調用子句```buf.String()```。也就是說,第62行代碼的累積取樣計數由兩部分組成,即函數```main.ShowDepStruct```的累積取樣計數和函數```bytes.(*Buffer).String```的累積取樣計數。同理,示例中的第57行代碼```fmt.Printf("%s\n", buf.String())```的累積取樣計數也存在偏差。讀者可以試著分析一下原因。 如果讀者想驗證上面所說的產生偏差的原因的話,可以將上面示例中的第62行代碼和第57行代碼分別拆成兩行,然后再對命令源碼文件showpds.go進行編譯、運行(記得加入相關標記)并用pprof工具的list命令進行查看。不過,驗證這個原因還有一個更簡便的方式——使用pprof工具中的disasm命令。我們在下面的示例中執行disasm命令并后跟routine_regexp參數值ShowDepStruct。 bash (pprof) disasm ShowDepStruct Total: 23 samples ROUTINE ====================== main.ShowDepStruct 0 20 samples (flat, cumulative) 87.0% of total -------------------- /home/hc/mybook/goc2p/src/helper/pds/showpds.go . . 49: func ShowDepStruct(pnode *pkgtool.PkgNode, prefix string) { <省略部分輸出內容> . 10 62: ShowDepStruct(v, buf.String()) . . 80490ce: MOVL main.&buf+3c(SP),AX . . 80490d2: XORL BX,BX . . 80490d4: CMPL BX,AX . . 80490d6: JNE main.ShowDepStruct+0x25f(SB) . . 80490d8: LEAL go.string.*+0x12d4(SB),BX . . 80490de: MOVL 0(BX),CX . . 80490e0: MOVL 4(BX),AX . . 80490e3: MOVL main.v+48(SP),BX . . 80490e7: MOVL BX,0(SP) . . 80490ea: MOVL CX,4(SP) . . 80490ee: MOVL AX,8(SP) . 10 80490f2: CALL main.ShowDepStruct(SB) . . 80490f7: MOVL main.autotmp_0046+44(SP),DX . . 80490fb: MOVL main.autotmp_0048+70(SP),CX . . 61: for _, v := range deps { . . 80490ff: INCL DX . . 8049100: MOVL main.autotmp_0047+2c(SP),BX . . 8049104: CMPL BX,DX . . 8049106: JLT main.ShowDepStruct+0x20b(SB) . . 64: } . . 8049108: ADDL $80,SP . . 804910e: RET . 2 62: ShowDepStruct(v, buf.String()) . . 804910f: MOVL 8(AX),DI . . 8049112: MOVL 4(AX),DX . . 8049115: MOVL c(AX),CX . . 8049118: CMPL CX,DX . . 804911a: JCC main.ShowDepStruct+0x273(SB) . . 804911c: CALL runtime.panicslice(SB) . . 8049121: UD2 . . 8049123: MOVL DX,SI . . 8049125: SUBL CX,SI . . 8049127: MOVL DI,DX . . 8049129: SUBL CX,DX . . 804912b: MOVL 0(AX),BP . . 804912d: ADDL CX,BP . . 804912f: MOVL BP,main.autotmp_0073+74(SP) . . 8049133: MOVL main.autotmp_0073+74(SP),BX . . 8049137: MOVL BX,0(SP) . . 804913a: MOVL SI,4(SP) . . 804913e: MOVL DX,8(SP) . 2 8049142: CALL runtime.slicebytetostring(SB) <省略部分輸出內容> (pprof) (pprof) 由于篇幅原因,我們只顯示了部分輸出內容。disasm命令與list命令的輸出內容有幾分相似。實際上,disasm命令在輸出函數```main.ShowDepStruct```的源碼的同時還在每一行代碼的下面列出了與這行代碼對應的匯編指令。并且,命令還在每一行的最左邊的對應位置上標注了該行匯編指令的本地取樣計數和累積取樣計數,同樣以英文句號“.”代表計數為0的情況。另外,在匯編指令的左邊且僅與匯編指令以一個冒號相隔的并不是像Go語言代碼行中那樣的行號,而是匯編指令對應的內存地址。 在上面這個示例中,我們只關注命令源碼文件showpds.go中的第62行代碼```ShowDepStruct(v, buf.String())``所對應的匯編指令。請讀者著重查看在累積取樣計數的列上有數字的行。像這樣的行一共有四個。為了方便起見,我們把這四行摘抄如下: . 10 62: ShowDepStruct(v, buf.String()) . 10 80490f2: CALL main.ShowDepStruct(SB) . 2 62: ShowDepStruct(v, buf.String()) . 2 8049142: CALL runtime.slicebytetostring(SB) 其中的第一行和第三行說明了第62行代碼的累積取樣計數的組成,而第二行和第四行說明了存在這樣的組成的原因。其中,匯編指令```CALL main.ShowDepStruct(SB)```的累積取樣計數為10。也就是說,調用main.ShowDepStruct函數期間分析器進行了10次取樣。而匯編指令```runtime.slicebytetostring(SB)```的累積取樣計數為2,意味著在調用函數runtime.slicebytetostring期間分析器進行了2次取樣。但是,```runtime.slicebytetostring```函數又是做什么用的呢?實際上,```runtime.slicebytetostring```函數正是被函數```bytes.(*Buffer).String```函數調用的。它實現的功能是把元素類型為byte的切片轉換為字符串。綜上所述,確實像我們之前說的那樣,命令源碼文件showpds.go中的第62行代碼```ShowDepStruct(v, buf.String())```的累積取樣計數12由函數```main.ShowDepStruct```的累積取樣計數10和函數```bytes.(*Buffer).String的```累積取樣計數2組成。 至此,我們介紹了三個非常有用的命令,它們是gv命令、list命令和disasm命令。我們可以通過gv命令以圖像化的方式查看程序中各個函數的本地取樣計數、累積取樣計數以及它們之間的調用關系和調用計數,并且可以很容易的通過節點面積的大小和有向線段的粗細找到計數值較大的節點。當我們依照之前所描述的分析過程找到可疑的高耗時的函數時,便可以使用list命令來查看函數內部各個代碼行的本地取樣計數和累積取樣計數情況,并能夠準確的找到使用了過多的CPU時間的代碼。同時,我們還可以使用disasm命令來查看函數中每行代碼所對應的匯編指令,并找到代碼耗時的根源所在。因此,只要我們適時配合使用上述的這三條命令,就幾乎可以在任何情況下理清程序性能問題的來龍去脈。可以說,它們是Go語言為我們提供的用于解決程序性能問題的瑞士軍刀。 但是,有時候我們只是想了解哪些函數花費的CPU時間最多。在這種情況下,前面講到的那幾個命令所產生的數據就顯得不那么直觀了。不過不要擔心,pprof工具為此提供了top命令。請看如下示例: bash (pprof) top Total: 23 samples 5 21.7% 21.7% 5 21.7% runtime.findfunc 5 21.7% 43.5% 5 21.7% stkbucket 3 13.0% 56.5% 3 13.0% os.(*File).write 1 4.3% 60.9% 1 4.3% MHeap_AllocLocked 1 4.3% 65.2% 1 4.3% getaddrbucket 1 4.3% 69.6% 2 8.7% runtime.MHeap_Alloc 1 4.3% 73.9% 1 4.3% runtime.SizeToClass 1 4.3% 78.3% 1 4.3% runtime.aeshashbody 1 4.3% 82.6% 1 4.3% runtime.atomicload64 1 4.3% 87.0% 1 4.3% runtime.convT2E (pprof) 在默認情況下,top命令會輸出以本地取樣計數為順序的列表。我們可以把這個列表叫做本地取樣計數排名列表。列表中的每一行都有六列。我們現在從左到右看,第一列和第二列的含義分別是:函數的本地取樣計數和該本地取樣計數在總取樣計數中所占的比例。第四列和第五列的含義分別是:函數的累積取樣計數和該累積取樣計數在總取樣計數中所占的比例。第五列的含義是左邊幾列數據所對應的函數的名稱。讀者應該對它們已經很熟悉了。這里需要重點說明的是第三列。第三列的含義是目前已打印出的函數的本地取樣計數之和在總取樣計數中所占的百分比。更具體的講,第三行第三列上的百分比值就是列表前三行的三個本地取樣計數的總和13除以總取樣計數23而得出的。我們還可以通過將第二行上的百分比值43.5%與第三行第二列上的百分比值13.0%相加得到第三行第三列上的百分比值。第三列的百分比值可以使我們很直觀的了解到最耗時的幾個函數總共花費掉的CPU時間的比重。我們可以利用這一比重為性能優化任務制定更加多樣化的目標。比如,我們的性能優化目標是把前四個函數的總耗時比重占比從60.9%降低到50%,等等。 從上面的示例我們可以看出,本地取樣計數較大的函數都屬于標準庫的代碼包或者Go語言內部。所以,我們無法或者不方便對這些函數進行優化。我們在之前提到過,在一般情況下,用戶程序中的函數的本地取樣計數都會非常低甚至是0。所以,如果我們編寫的函數處在本地取樣計數排名列表中的前幾名的位置上話,就說明這個函數可能存在著性能問題。這時就需要我們通過list命令產生針對于這個函數的數據并仔細進行分析。舉個例子,如果我們在函數中加入了一些并發控制代碼(不論是同步并發控制還是異步的并發控制)使得這個函數本身的執行時間很長并在本地取樣計數排名列表中處于前幾名的位置,那么我們就應該仔細查看該函數中各行代碼的取樣計數以及它們的邏輯合理性。比如,用于同步并發控制的代碼中是否存在產生死鎖的可能性,或者用于異步并發控制的代碼中是否存在協調失衡或者資源分配不均的地方。與編寫合理和優秀的并發控制代碼有關的內容在本書的第三部分。 在默認情況下,top命令輸出的列表中只包含本地取樣計數最大的前十個函數。如果我們想自定義這個列表的項數,那么需要在top命令后面緊跟一個項數值。比如:命令top5會輸出行數為5的列表,命令top20會輸出行數為20的列表,等等。 如果我們在top命令后加入標簽```--cum```,那么輸出的列表就是以累積取樣計數為順序的。示例如下: (pprof) top20 --cum Total: 23 samples 0 0.0% 0.0% 23 100.0% gosched0 0 0.0% 0.0% 22 95.7% main.main 0 0.0% 0.0% 22 95.7% runtime.main 0 0.0% 0.0% 16 69.6% runtime.mallocgc 0 0.0% 0.0% 12 52.2% pkgtool.(*PkgNode).Grow 0 0.0% 0.0% 11 47.8% runtime.MProf_Malloc 0 0.0% 0.0% 10 43.5% main.ShowDepStruct 0 0.0% 0.0% 10 43.5% pkgtool.getImportsFromPackage 0 0.0% 0.0% 8 34.8% cnew 0 0.0% 0.0% 8 34.8% makeslice1 0 0.0% 0.0% 8 34.8% runtime.cnewarray 0 0.0% 0.0% 7 30.4% gostringsize 0 0.0% 0.0% 7 30.4% runtime.slicebytetostring 0 0.0% 0.0% 6 26.1% pkgtool.getImportsFromGoSource 0 0.0% 0.0% 6 26.1% runtime.callers 1 4.3% 4.3% 6 26.1% runtime.gentraceback 0 0.0% 4.3% 6 26.1% runtime.makeslice 5 21.7% 26.1% 5 21.7% runtime.findfunc 5 21.7% 47.8% 5 21.7% stkbucket 0 0.0% 47.8% 4 17.4% fmt.Fprintf (pprof) 我們可以把這類列表叫做累積取樣計數排名列表。在這個列表中,有命令源碼文件showpds.go和代碼包pkgtool中的函數上榜。它們都存在于項目goc2p中。在實際場景中,用戶程序中的函數一般都處于函數調用關系圖的上游。尤其是命令源碼文件的入口函數```main.main```。所以,它們的累積取樣計數一般都比較大,即使在累積取樣計數排名列表中名列前茅也不足為奇。不過,如果一個函數的累積取樣計數和百分比都很大,就應該引起我們的注意了。這在前面講解gv命令的時候也有所提及。如果我們想在排名列表中過濾掉一些我們不關注的函數,還可以在命令的最后追加一個或多個我們想忽略的函數的名稱或相應的正則表達式。像這樣: (pprof) top20 --cum -fmt\..* -os\..* Ignoring samples in call stacks that match 'fmt\..*|os\..*' Total: 23 samples After ignoring 'fmt\..*|os\..*': 15 samples of 23 (65.2%) 0 0.0% 0.0% 15 65.2% gosched0 0 0.0% 0.0% 14 60.9% main.main 0 0.0% 0.0% 14 60.9% runtime.main 0 0.0% 0.0% 12 52.2% runtime.mallocgc 0 0.0% 0.0% 8 34.8% pkgtool.(*PkgNode).Grow 0 0.0% 0.0% 7 30.4% gostringsize 0 0.0% 0.0% 7 30.4% pkgtool.getImportsFromPackage 0 0.0% 0.0% 7 30.4% runtime.MProf_Malloc 0 0.0% 0.0% 7 30.4% runtime.slicebytetostring 0 0.0% 0.0% 6 26.1% main.ShowDepStruct 0 0.0% 0.0% 6 26.1% pkgtool.getImportsFromGoSource 0 0.0% 0.0% 5 21.7% cnew 0 0.0% 0.0% 5 21.7% makeslice1 0 0.0% 0.0% 5 21.7% runtime.cnewarray 0 0.0% 0.0% 4 17.4% runtime.callers 1 4.3% 4.3% 4 17.4% runtime.gentraceback 0 0.0% 4.3% 3 13.0% MCentral_Grow 0 0.0% 4.3% 3 13.0% runtime.MCache_Alloc 0 0.0% 4.3% 3 13.0% runtime.MCentral_AllocList 3 13.0% 17.4% 3 13.0% runtime.findfunc (pprof) 在上面的示例中,我們通過命令top20獲取累積取樣計數最大的20個函數的信息,同時過濾掉了來自代碼包```fmt```和```os```中的函數。 我們要詳細講解的最后一個命令是callgrind。pprof工具可以將概要轉化為強大的Valgrind工具集中的組件Callgrind支持的格式。Valgrind是可運行在Linux操作系統上的用來成分析程序性能及程序中的內存泄露錯誤的強力工具。而作為其中組件之一的Callgrind的功能是收集程序運行時的一些數據、函數調用關系等信息。由此可知,Callgrind工具的功能基本上與我們之前使用標準庫代碼包runtime的API對程序運行情況進行取樣的操作是一致的。 我們可以通過callgrind命令將概要文件的內容轉化為Callgrind工具可識別的格式并保存到文件中。示例如下: (pprof) callgrind cpu.callgrind Writing callgrind file to 'cpu.callgrind'. (pprof) 文件cpu.callgrind是一個普通文本文件,所以我們可以使用任何文本查看器來查看其中的內容。但更方便的是,我們可以使用callgrind命令直接查看到圖形化的數據。現在我們來嘗試一下: (pprof) callgrind Writing callgrind file to '/tmp/pprof2641.0.callgrind'. Starting 'kcachegrind /tmp/pprof2641.0.callgrind & ' (pprof) sh: 1: kcachegrind: not found 我們沒有在callgrind命令后添加任何作為參數的統計文件路徑。所以callgrind命令會自行使用kcachegrind工具以可視化的方式顯示統計數據。然而,我們的系統中還沒有安裝kcachegrind工具。 在Debian的Linux發行版及其衍生版下,我們可以直接使用命令```sudo apt-get install kcachegrind```來安裝kcachegrind工具。或者我們可以從[其官方網站](http://kcachegrind.sourceforge.net/)下載安裝包來進行安裝。 安裝好kcachegrind工具之后,我們再來執行callgrind命令: bash (pprof) callgrind Writing callgrind file to '/tmp/pprof2641.1.callgrind'. Starting 'kcachegrind /tmp/pprof2641.1.callgrind & ' (pprof) 從命令輸出的提示信息可以看出,實際上callgrind命令把統計文件保存到了Linux的臨時文件夾/tmp中。然后使用kcachegrind工具進行查看。下圖為在pprof工具交互模式下執行callgrind命令后彈出的kcachegrind工具界面。 ![使用kcachegrind工具查看概要數據](https://box.kancloud.cn/db7800083e66d0779f39b6269275c006_1300x634.jpg) _圖0-4 使用kcachegrind工具查看概要數據_ 從上圖中我們可以看到,kcachegrind工具對數據的展現非常直觀。總體上來說,界面被分為了左右兩欄。在左欄中的是概要文件中記錄的函數的信息列表。列表一共有五列,從左到右的含義 分別是函數的累積取樣計數在總取樣計數中的百分比、函數的本地取樣計數在總取樣計數中的百分比、函數被調用的總次數(包括遞歸調用)、函數的名稱以及函數所在的源碼文件的名稱。而在界面的右欄,我們查看在左欄中選中的行的詳細信息。kcachegrind工具的功能非常強大。不過由于對它的介紹超出了本書的范圍,所以我們就此暫告一個段落。 我們剛剛提到過,不加任何參數callgrind命令的執行分為兩個步驟——生成統計文件和使用kcachegrind工具查看文件內容。還記得我們在之前已經生成了一個名為統計文件cpu.callgrind嗎?其實,我們可以使用命令```kcachegrind cpu.callgrind```直接對它進行查看。執行這個命令后所彈出的kcachegrind工具界面與我們之前看到的完全一致。 到現在為止,我們又介紹了兩個可以更直觀的統計和查看概要文件中數據的命令。top命令讓我們可以在命令行終端中查看這些統計信息。而callgrind命令使我們通過kcachegrind工具查看概要文件的數據成為了可能。這兩個命令都讓我們可以宏觀的、從不同維度的來查看和分析概要文件。它們都是非常給力的統計輔助工具。 除了上面詳細講述的那些命令之外,pprof工具在交互模式下還支持少許其它的命令。這在表5-23中也有所體現。這些命令有的只是主要命令的另一種形式(比如web命令和weblist命令),而有的只是為了提供輔助功能(比如help命令和quit命令)。 在本小節中,我們只使用```go tool pprof```命令對CPU概要文件進行了查看和分析。讀者可以試著對內存概要文件和程序阻塞概要文件進行分析。 相對于普通的編程方式來講,并發編程都是復雜的。所以,我們就更需要像pprof這樣的工具為我們保駕護航。大家可以將本小節當作一篇該工具的文檔,并在需要時隨時查看。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看