Phân tích hiệu suất
Khi một chương trình được viết xong yêu cầu của chúng ta đối với nó không chỉ là có thể chạy mà còn hy vọng nó là một ứng dụng ổn định và hiệu quả. Thông qua nhiều loại kiểm tra khác nhau chúng ta có thể đảm bảo phần lớn tính ổn định của chương trình còn việc chương trình có hiệu quả hay không cần chúng ta phân tích hiệu suất cho nó trong nội dung trước đó phương tiện duy nhất để phân tích hiệu suất chương trình chỉ có thể thông qua Benchmark để kiểm tra thời gian thực thi trung bình tình hình phân bổ bộ nhớ của một đơn vị chức năng nào đó tuy nhiên trong thực tế nhu cầu phân tích hiệu suất chương trình vượt xa điều này đôi khi chúng ta cần phân tích mức sử dụng CPU tổng thể của chương trình mức sử dụng bộ nhớ tình hình phân bổ heap trạng thái goroutine đường dẫn mã nguồn nóng v.v. đây là những gì Benchmark không thể đáp ứng được. May mắn thay chuỗi công cụ go đã tích hợp nhiều công cụ phân tích hiệu suất để nhà phát triển sử dụng dưới đây sẽ lần lượt giải thích.
Phân tích escape
Trong go việc phân bổ bộ nhớ của biến do trình biên dịch quyết định nói chung là phân bổ trên stack và heap. Nếu một biến đáng lẽ nên phân bổ trên stack lại được phân bổ trên heap thì trường hợp này được gọi là escape phân tích escape là để phân tích tình hình phân bổ bộ nhớ trong chương trình vì nó được thực hiện trong thời gian biên dịch nên là một loại phân tích tĩnh.
TIP
Đến bài viết Phân bổ bộ nhớ để hiểu go cụ thể phân bổ bộ nhớ như thế nào.
Con trỏ local tham chiếu
package main
func main() {
GetPerson()
}
type Person struct {
Name string
Mom *Person
}
func GetPerson() Person {
mom := Person{Name: "lili"}
son := Person{Name: "jack", Mom: &mom}
return son
}Hàm GetPerson tạo biến mom vì nó được tạo trong hàm lẽ ra nên phân bổ nó trên stack nhưng nó được trường Mom của son tham chiếu và son được làm giá trị trả về của hàm trả ra ngoài nên trình biên dịch phân bổ nó trên heap. Đây là một ví dụ rất đơn giản nên hiểu không cần tốn quá nhiều sức nhưng nếu là một dự án lớn hơn một chút số dòng mã có hàng vạn dòng việc phân tích thủ công sẽ không dễ dàng như vậy do đó cần sử dụng công cụ để phân tích escape. Như đã đề cập trước đó việc phân bổ bộ nhớ do trình biên dịch chủ đạo nên phân tích escape cũng do trình biên dịch hoàn thành sử dụng rất đơn giản chỉ cần thực hiện lệnh sau
$ go build -gcflags="-m -m -l"gcflags tức tham số của trình biên dịch gc
-min ra đề xuất tối ưu hóa mã đồng thời xuất hiện hai cái sẽ xuất chi tiết hơn-lvô hiệu hóa tối ưu hóa inline
Kết quả xuất như sau
$ go build -gcflags="-m -m -l" .
# golearn/example
./main.go:13:2: mom escapes to heap:
./main.go:13:2: flow: son = &mom:
./main.go:13:2: from &mom (address-of) at ./main.go:14:35
./main.go:13:2: from Person{...} (struct literal element) at ./main.go:14:15
./main.go:13:2: from son := Person{...} (assign) at ./main.go:14:6
./main.go:13:2: flow: ~r0 = son:
./main.go:13:2: from return son (return) at ./main.go:15:2
./main.go:13:2: moved to heap: momTrình biên dịch nói rõ cho chúng ta biết biến mom đã xảy ra escape lý do dẫn đến là vì giá trị trả về bao gồm con trỏ local của hàm ngoài tình huống này ra còn có các tình huống khác có thể xảy ra hiện tượng escape
::: tips
Nếu bạn quan tâm đến chi tiết của phân tích escape có thể tìm hiểu thêm nội dung trong cmd/compile/internal/escape/escape.go của thư viện chuẩn.
:::
Closure tham chiếu
Closure tham chiếu biến bên ngoài hàm thì biến đó cũng sẽ escape lên heap điều này rất dễ hiểu.
package main
func main() {
a := make([]string, 0)
do(func() []string {
return a
})
}
func do(f func() []string) []string {
return f()
}Kết quả xuất
$ go build -gcflags="-m -m -l" .
# golearn/example
./main.go:10:9: f does not escape
./main.go:4:2: main capturing by value: a (addr=false assign=false width=24)
./main.go:4:11: make([]string, 0) escapes to heap:
./main.go:4:11: flow: a = &{storage for make([]string, 0)}:
./main.go:4:11: from make([]string, 0) (spill) at ./main.go:4:11
./main.go:4:11: from a := make([]string, 0) (assign) at ./main.go:4:4
./main.go:4:11: flow: ~r0 = a:
./main.go:4:11: from return a (return) at ./main.go:6:3
./main.go:4:11: make([]string, 0) escapes to heap
./main.go:5:5: func literal does not escapeKhông đủ không gian
Khi không gian stack không đủ cũng sẽ xảy ra hiện tượng escape slice được tạo dưới đây đã xin dung lượng 1<<15
package main
func main() {
_ = make([]int, 0, 1<<15)
}Kết quả xuất
$ go build -gcflags="-m -m -l" .
# golearn/example
./main.go:4:10: make([]int, 0, 32768) escapes to heap:
./main.go:4:10: flow: {heap} = &{storage for make([]int, 0, 32768)}:
./main.go:4:10: from make([]int, 0, 32768) (too large for stack) at ./main.go:4:10
./main.go:4:10: make([]int, 0, 32768) escapes to heapĐộ dài không xác định
Khi độ dài của slice là một biến thì do độ dài không xác định nên sẽ xảy ra hiện tượng escape (map sẽ không)
package main
func main() {
n := 100
_ = make([]int, n)
}Kết quả xuất
$ go build -gcflags="-m -m -l" .
# golearn/example
./main.go:5:10: make([]int, n) escapes to heap:
./main.go:5:10: flow: {heap} = &{storage for make([]int, n)}:
./main.go:5:10: from make([]int, n) (non-constant size) at ./main.go:5:10
./main.go:5:10: make([]int, n) escapes to heapCòn có một trường hợp đặc biệt là khi tham số hàm là loại ...any cũng có thể xảy ra escape
package main
import "fmt"
func main() {
n := 100
fmt.Println(n)
}Kết quả xuất
$ go build -gcflags="-m -m -l" .
# golearn/example
./main.go:7:14: n escapes to heap:
./main.go:7:14: flow: {storage for ... argument} = &{storage for n}:
./main.go:7:14: from n (spill) at ./main.go:7:14
./main.go:7:14: from ... argument (slice-literal-element) at ./main.go:7:13
./main.go:7:14: flow: {heap} = {storage for ... argument}:
./main.go:7:14: from ... argument (spill) at ./main.go:7:13
./main.go:7:14: from fmt.Println(... argument...) (call parameter) at ./main.go:7:13
./main.go:7:13: ... argument does not escape
./main.go:7:14: n escapes to heapLý do chúng ta cần phân tích escape kiểm soát phân bổ bộ nhớ chi tiết như vậy chủ yếu là để giảm áp lực cho GC nhưng go không phải là ngôn ngữ C quyền quyết định cuối cùng của phân bổ bộ nhớ vẫn nằm trong tay trình biên dịch ngoài tình huống yêu cầu hiệu suất cực đoan phần lớn thời gian chúng ta cũng không cần quá chuyên tâm vào chi tiết phân bổ bộ nhớ dù sao mục đích sinh ra của GC là để giải phóng nhà phát triển.
Chi tiết nhỏ
Đối với một số loại tham chiếu khi xác định sau này không còn dùng đến nó nữa chúng ta có thể đặt nó thành nil để nói cho GC biết có thể thu hồi nó.
type Writer struct {
buf []byte
}
func (w Writer) Close() error {
w.buff = nil
return nil
}pprof
pprof (program profiling) là một công cụ mạnh mẽ để phân tích hiệu suất chương trình nó sẽ lấy mẫu một phần dữ liệu runtime của chương trình bao gồm cpu bộ nhớ goroutine lock thông tin stack và nhiều khía cạnh khác sau đó sử dụng công cụ để phân tích dữ liệu đã lấy mẫu và hiển thị kết quả.
Do đó các bước sử dụng pprof chỉ có hai bước
- Thu thập dữ liệu
- Phân tích kết quả
Thu thập
Có hai cách thu thập dữ liệu tự động và thủ công mỗi cách có ưu nhược điểm. Trước đó viết một hàm đơn giản để mô phỏng tiêu thụ bộ nhớ và cpu
func Do() {
for i := 0; i < 10; i++ {
slice := makeSlice()
sortSlice(slice)
}
}
func makeSlice() []int {
var s []int
for range 1 << 24 {
s = append(s, rand.Int())
}
return s
}
func sortSlice(s []int) {
slices.Sort(s)
}Thủ công
Thu thập thủ công là kiểm soát thông qua mã ưu điểm là có thể kiểm soát linh hoạt có thể tùy chỉnh trực tiếp sử dụng pprof trong mã cần import gói runtime/pprof
package main
import (
"log"
"os"
"runtime/pprof"
)
func main() {
Do()
w, _ := os.Create("heap.pb")
heapProfile := pprof.Lookup("heap")
err := heapProfile.WriteTo(w, 0)
if err != nil {
log.Fatal(err)
}
}Các tham số được hỗ trợ bởi pprof.Lookup như mã dưới đây
profiles.m = map[string]*Profile{
"goroutine": goroutineProfile,
"threadcreate": threadcreateProfile,
"heap": heapProfile,
"allocs": allocsProfile,
"block": blockProfile,
"mutex": mutexProfile,
}Hàm này sẽ ghi dữ liệu thu thập được vào file chỉ định khi ghi số truyền vào có các ý nghĩa sau
0ghi dữ liệu Protobuf đã nén không có khả năng đọc1ghi dữ liệu định dạng văn bản có thể đọc được giao diện http trả về là loại dữ liệu này2chỉgoroutinecó thể sử dụng biểu thị in thông tin stack phong cáchpanic
Thu thập dữ liệu cpu cần sử dụng riêng hàm pprof.StartCPUProfile nó cần một khoảng thời gian để lấy mẫu và dữ liệu gốc không thể đọc được như sau
package main
import (
"log"
"os"
"runtime/pprof"
"time"
)
func main() {
Do()
w, _ := os.Create("cpu.out")
err := pprof.StartCPUProfile(w)
if err != nil {
log.Fatal(err)
}
time.Sleep(time.Second * 10)
pprof.StopCPUProfile()
}Thu thập dữ liệu trace cũng tương tự
package main
import (
"log"
"os"
"runtime/trace"
"time"
)
func main() {
Do()
w, _ := os.Create("trace.out")
err := trace.Start(w)
if err != nil {
log.Fatal(err)
}
time.Sleep(time.Second * 10)
trace.Stop()
}Tự động
Gói net/http/pprof đóng gói các hàm phân tích ở trên thành giao diện http và đăng ký vào route mặc định như sau
package pprof
import ...
func init() {
http.HandleFunc("/debug/pprof/", Index)
http.HandleFunc("/debug/pprof/cmdline", Cmdline)
http.HandleFunc("/debug/pprof/profile", Profile)
http.HandleFunc("/debug/pprof/symbol", Symbol)
http.HandleFunc("/debug/pprof/trace", Trace)
}Điều này cho phép chúng ta trực tiếp chạy thu thập dữ liệu pprof bằng một nút
package main
import (
"net/http"
// Nhớ import gói này
_ "net/http/pprof"
)
func main() {
go func(){
http.ListenAndServe(":8080", nil)
}
for {
Do()
}
}Lúc này mở trình duyệt truy cập http://127.0.0.1:8080/debug/pprof sẽ xuất hiện trang như thế này

Trong trang có vài tùy chọn có thể chọn chúng lần lượt đại diện cho
allocslấy mẫu phân bổ bộ nhớblocktheo dõi block của nguyên lý đồng bộcmdlinegọi dòng lệnh của chương trình hiện tạigoroutinetheo dõi tất cả goroutineheaplấy mẫu phân bổ bộ nhớ cho các đối tượng đang tồn tạimutextheo dõi thông tin liên quan đến mutexprofilephân tích cpu sẽ phân tích một khoảng thời gian và tải xuống một filethreadcreatephân tích lý do dẫn đến việc tạo thread OS mớitracetheo dõi tình hình thực thi của chương trình hiện tại cũng sẽ tải xuống một file
Dữ liệu ở đây phần lớn khả năng đọc không cao chủ yếu là để công cụ phân tích sử dụng như hình dưới

Công việc phân tích cụ thể để lại sau này thực hiện ngoài hai tùy chọn profile và trace nếu bạn muốn tải xuống file dữ liệu trong trang web có thể bỏ tham số query là debug=1. Cũng có thể tích hợp các giao diện này vào route của bạn thay vì sử dụng route mặc định như sau
package main
import (
"net/http"
"net/http/pprof"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/trace", pprof.Trace)
servre := &http.Server{
Addr: ":8080",
Handler: mux,
}
servre.ListenAndServe()
}Như vậy cũng có thể tích hợp vào các framework web khác như gin iris v.v.
Phân tích
Sau khi có được file dữ liệu thu thập có hai cách để phân tích dòng lệnh hoặc trang web cả hai đều cần借助 công cụ dòng lệnh pprof go mặc định tích hợp công cụ này nên không cần tải xuống thêm.
Mã nguồn mở của pprof google/pprof: pprof is a tool for visualization and analysis of profiling data (github.com)
Dòng lệnh
Lấy file dữ liệu thu thập được trước đó làm tham số
$ go tool pprof heap.pbNếu dữ liệu do web thu thập thì dùng web url thay thế tên file là được.
$ go tool pprof -http :8080 http://127.0.0.1/debug/pprof/heapSau đó sẽ xuất hiện một dòng lệnh tương tác
15:27:38.3266862 +0800 CST
Type: inuse_space
Time: Apr 15, 2024 at 3:27pm (CST)
No samples were found with the default sample value type.
Try "sample_index" command to analyze different sample values.
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)Nhập help có thể xem các lệnh khác
Commands:
callgrind Outputs a graph in callgrind format
comments Output all profile comments
disasm Output assembly listings annotated with samples
dot Outputs a graph in DOT format
eog Visualize graph through eog
evince Visualize graph through evince
...Trong dòng lệnh xem dữ liệu nói chung sử dụng lệnh top cũng có thể dùng lệnh traces nhưng kết quả xuất của nó rất dài dòng lệnh top chỉ đơn giản xem qua đại khái.
(pprof) top 5
Showing nodes accounting for 117.49MB, 100% of 117.49MB total
flat flat% sum% cum cum%
117.49MB 100% 100% 117.49MB 100% main.makeSlice (inline)
0 0% 100% 117.49MB 100% main.Do
0 0% 100% 117.49MB 100% main.main
0 0% 100% 117.49MB 100% runtime.mainGiới thiệu đơn giản một số chỉ tiêu trong đó (cpu tương tự)
flatđại diện cho tài nguyên mà hàm hiện tại tiêu thụcumtổng tài nguyên tiêu thụ của hàm hiện tại và chuỗi gọi tiếp theo của nóflat%flat/totalcum%cum/total
Chúng ta có thể thấy rõ tổng mức sử dụng bộ nhớ của toàn bộ stack gọi là 117.49MB vì hàm Do tự nó không làm gì cả chỉ gọi các hàm khác nên chỉ số flat của nó là 0 việc tạo slice do hàm makeSlice phụ trách nên chỉ số flat của nó là 100%.
Chúng ta có thể chuyển đổi thành định dạng trực quan hóa pprof hỗ trợ khá nhiều định dạng như pdf svg png gif v.v. (cần cài đặt Graphviz).
(pprof) png
Generating report in profile001.png
Thông qua hình ảnh chúng ta có thể thấy rõ hơn tình hình bộ nhớ của toàn bộ stack gọi.
Thông qua lệnh list để xem dưới dạng mã nguồn
(pprof) list Do
Total: 117.49MB
ROUTINE ======================== main.Do in D:\WorkSpace\Code\GoLeran\golearn\example\main.go
0 117.49MB (flat, cum) 100% of Total
. . 21:func Do() {
. . 22: for i := 0; i < 10; i++ {
. 117.49MB 23: slice := makeSlice()
. . 24: sortSlice(slice)
. . 25: }
. . 26:}
. . 27:
. . 28:func makeSlice() []int {Đối với hình ảnh và mã nguồn còn có thể dùng lệnh web và weblist để xem hình ảnh và mã nguồn trong trình duyệt.
Trang web
Trước đó để dữ liệu đa dạng hơn sửa đổi một chút hàm mô phỏng
func Do1() {
for i := 0; i < 10; i++ {
slice := makeSlice()
sortSlice(slice)
}
}
func Do2() {
for i := 0; i < 10; i++ {
slice := makeSlice()
sortSlice(slice)
}
}
func makeSlice() []int {
var s []int
for range 1 << 12 {
s = append(s, rand.Int())
}
return s
}
func sortSlice(s []int) {
slices.Sort(s)
}Phân tích trang web có thể trực quan hóa kết quả miễn chúng ta thao tác dòng lệnh thủ công khi sử dụng phân tích trang web chỉ cần thực hiện lệnh sau
$ go tool pprof -http :8080 heap.pbNếu dữ liệu do web thu thập thì thay web url vào tên file là được
$ go tool pprof -http :8080 http://127.0.0.1:9090/debug/pprof/heap
$ go tool pprof -http :8080 http://127.0.0.1:9090/debug/pprof/profile
$ go tool pprof -http :8080 http://127.0.0.1:9090/debug/pprof/goroutineTIP
Về cách phân tích dữ liệu đến pprof: How to read the graph để biết thêm

Trong trang web tổng cộng có 6 mục có thể xem
- Top giống lệnh top
- Graph biểu đồ đường thẳng
- Flame Graph biểu đồ ngọn lửa
- Peek
- Source xem mã nguồn
- Disassemble dịch ngược
Đối với bộ nhớ có bốn chiều có thể phân tích
alloc_objectssố lượng tất cả đối tượng đã phân bổ hiện tại bao gồm đã giải phóngalloc_spcaetất cả không gian bộ nhớ đã phân bổ cho đến nay bao gồm đã giải phónginuse_objectssố lượng đối tượng đang sử dụnginuse_spacekhông gian bộ nhớ đang sử dụng

Các nút lá màu trắng ở dưới cùng của hình trên đại diện cho các đối tượng có kích thước khác nhau chiếm dụng.

Về biểu đồ折 tuyến có vài điểm cần lưu ý
- Màu của khối càng đậm mức sử dụng càng cao đường càng粗 mức sử dụng càng cao
- Đường liền đại diện cho gọi trực tiếp đường nét đứt đại diện cho bỏ qua một số chuỗi gọi.


Đối với biểu đồ ngọn lửa nhìn từ trên xuống dưới là chuỗi gọi nhìn từ trái sang phải là tỷ lệ phần trăm chiếm dụng cum.
trace
pprof chủ yếu phụ trách phân tích mức sử dụng tài nguyên của chương trình còn trace phù hợp hơn để theo dõi chi tiết thực thi của chương trình nó không tương thích với file dữ liệu của cái trước công việc phân tích liên quan do lệnh go tool trace hoàn thành.
Nếu là dữ liệu thu thập thủ công có thể lấy tên file làm tham số
$ go tool trace trace.outNếu là thu thập tự động cũng tương tự
$ curl http://127.0.0.1:8080/debug/pprof/trace > trace.out && go tool trace trace.outThực hiện sau sẽ khởi động một web server
2024/04/15 17:15:40 Preparing trace for viewer...
2024/04/15 17:15:40 Splitting trace for viewer...
2024/04/15 17:15:40 Opening browser. Trace viewer is listening on http://127.0.0.1:51805Mở ra trang đại khái như sau

Trong này chủ yếu bao gồm các phần sau dữ liệu này muốn xem hiểu còn khá không dễ dàng.
Event timelines for running goroutines
trace by proc hiển thị timeline của goroutine chạy trên bộ xử lý đó tại mỗi thời điểm

trace by thread hiển thị timeline của goroutine chạy trên thread OS tại mỗi thời điểm

Goroutine analysis hiển thị thông tin thống kê liên quan đến goroutine của mỗi nhóm hàm chính


Profiles
- Network blocking profile thông tin goroutine bị block do IO mạng
- Synchronization blocking profile thông tin goroutine bị block do nguyên lý đồng bộ
- Syscall profile thông tin goroutine bị block do gọi hệ thống
User-defined tasks and regions
- User-defined tasks thông tin goroutine liên quan đến task do người dùng định nghĩa
- User-defined regions thông tin goroutine liên quan đến vùng mã do người dùng định nghĩa
Garbage collection metrics
Minimum mutator utilization hiển thị thời gian tiêu thụ tối đa của GC gần đây

