Memory and Performance
Chúng ta sẽ khám phá Pointer, Garbage Collection, và Memory Leaks & Profiling với độ chi tiết cao hơn, kèm theo các ví dụ phức tạp và phân tích.
1. Pointer (&
và *
) - Hiểu sâu và ứng dụng thực tế
&
và *
) - Hiểu sâu và ứng dụng thực tếHiểu sâu về Pointer:
Pointer trong Go không chỉ là công cụ để "truyền tham chiếu", mà còn liên quan chặt chẽ đến escape analysis và cách Go quản lý bộ nhớ.
Escape Analysis: Trình biên dịch Go quyết định biến được cấp phát trên stack hay heap. Nếu một biến được truyền qua pointer và "thoát" khỏi phạm vi stack (ví dụ: trả về từ hàm), nó sẽ được cấp phát trên heap.
Ví dụ nâng cao:
package main
import "fmt"
type LargeStruct struct {
Data [1000]int // Struct lớn để minh họa
}
func noEscape(s LargeStruct) {
s.Data[0] = 42 // Copy toàn bộ struct, không dùng pointer
}
func withPointer(s *LargeStruct) {
s.Data[0] = 42 // Chỉ truyền địa chỉ, không copy
}
func escapePointer() *LargeStruct {
s := LargeStruct{}
return &s // s "thoát" khỏi stack, được cấp phát trên heap
}
func main() {
s := LargeStruct{}
noEscape(s) // Stack allocation
withPointer(&s) // Stack allocation (vì không thoát)
p := escapePointer() // Heap allocation
fmt.Println(p.Data[0])
}
Phân tích:
noEscape
: Truyền giá trị, copy toàn bộ struct (1000 int ≈ 8KB trên 64-bit). Không hiệu quả với dữ liệu lớn.withPointer
: Truyền pointer, chỉ 8 byte (địa chỉ). Hiệu quả hơn, nhưng vẫn trên stack nếu không thoát.escapePointer
: Trả về pointer, buộc cấp phát trên heap, ảnh hưởng đến GC.
Ứng dụng thực tế:
Mutex và Pointer: Trong lập trình đồng thời, pointer thường được dùng với
sync.Mutex
để bảo vệ dữ liệu.
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func main() {
c := &Counter{} // Pointer để chia sẻ giữa goroutine
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
c.Inc()
wg.Done()
}()
}
wg.Wait()
fmt.Println(c.value) // 100
}
Nil Pointer Panic: Pointer có thể là
nil
, cần kiểm tra cẩn thận:
var p *int
*p = 42 // Panic: nil pointer dereference
Best Practices:
Chỉ dùng pointer khi cần thay đổi dữ liệu gốc hoặc tối ưu hiệu suất.
Tránh lạm dụng pointer với dữ liệu nhỏ (int, bool), vì copy thường rẻ hơn quản lý pointer.
Hiểu escape analysis qua lệnh
go build -gcflags="-m"
.
2. Garbage Collection (GC) - Cơ chế nội tại và tối ưu hóa
Cơ chế chi tiết:
Go dùng Tri-color Mark-and-Sweep GC:
Black: Đã được đánh dấu, vẫn đang sử dụng.
Grey: Đang chờ kiểm tra tham chiếu.
White: Không được tham chiếu, sẽ bị thu hồi.
Write Barrier: Đảm bảo tính chính xác khi GC chạy đồng thời với chương trình (concurrent).
Pacing Algorithm: Từ Go 1.5, GC tự điều chỉnh tần suất dựa trên
GOGC
và tải ứng dụng.
Tối ưu hóa GC:
Giảm áp lực GC:
Tránh cấp phát heap không cần thiết (dùng stack khi có thể).
Tái sử dụng bộ nhớ với
sync.Pool
cho các đối tượng tạm thời.
Đo lường hiệu suất GC:
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
start := time.Now()
var m runtime.MemStats
// Tạo áp lực cho GC
for i := 0; i < 1000000; i++ {
_ = make([]byte, 1024) // Cấp phát 1KB mỗi lần
}
runtime.ReadMemStats(&m)
fmt.Printf("HeapAlloc: %v bytes\n", m.HeapAlloc)
fmt.Printf("GCCycles: %v\n", m.NumGC)
fmt.Printf("Time: %v\n", time.Since(start))
}
Ứng dụng thực tế:
sync.Pool để giảm cấp phát:
package main
import (
"fmt"
"sync"
)
var pool = sync.Pool{
New: func() interface{} {
return make([]byte, 1024)
},
}
func main() {
buf := pool.Get().([]byte) // Lấy từ pool
buf[0] = 42
pool.Put(buf) // Trả lại pool
fmt.Println("Done")
}
Giảm cấp phát lặp lại, đặc biệt trong server xử lý request lớn.
Tuning GOGC:
High Throughput:
GOGC=off
(tắt GC, dùng thủ công) hoặcGOGC=1000
.Low Latency:
GOGC=10
, GC chạy thường xuyên hơn.
Debug GC:
Dùng
runtime/debug
để ghi log:
import "runtime/debug"
debug.SetGCPercent(50) // Tương đương GOGC=50
3. Memory Leaks & Profiling (pprof
) - Phân tích chuyên sâu
pprof
) - Phân tích chuyên sâuNguyên nhân Memory Leak nâng cao:
Goroutine Leak: Goroutine không thoát do channel bị chặn hoặc context không hủy.
Slice/Map Growth: Slice giữ tham chiếu cũ khi cắt (do capacity không giảm).
Finalizer Misuse: Dùng
runtime.SetFinalizer
không đúng cách.
Ví dụ Goroutine Leak:
package main
import (
"fmt"
"time"
)
func leakyGoroutine() {
ch := make(chan int)
go func() {
for range ch { // Chờ mãi vì ch không đóng
}
}()
}
func main() {
for i := 0; i < 1000; i++ {
leakyGoroutine()
}
time.Sleep(1 * time.Second)
fmt.Println("Goroutines:", runtime.NumGoroutine()) // ~1000
}
Sửa bằng Context:
package main
import (
"context"
"fmt"
"time"
)
func fixedGoroutine(ctx context.Context) {
ch := make(chan int)
go func() {
select {
case <-ch:
case <-ctx.Done(): // Thoát khi context hủy
}
}()
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
for i := 0; i < 1000; i++ {
fixedGoroutine(ctx)
}
cancel() // Hủy tất cả goroutine
time.Sleep(1 * time.Second)
fmt.Println("Goroutines:", runtime.NumGoroutine()) // Gần 1
}
Profiling với pprof
:
Heap Profile:
Chạy:
go tool pprof http://localhost:6060/debug/pprof/heap
.Dùng
list <function>
để xem chi tiết cấp phát trong hàm.
Goroutine Profile:
Chạy:
go tool pprof http://localhost:6060/debug/pprof/goroutine
.Xác định goroutine bị treo qua stack trace.
Trace:
Chạy:
curl -o trace.out http://localhost:6060/debug/pprof/trace?seconds=5
.Mở:
go tool trace trace.out
để xem timeline.
Ví dụ thực tế với pprof
:
package main
import (
"net/http"
_ "net/http/pprof"
)
func expensive() {
for i := 0; i < 1000000; i++ {
_ = make([]byte, 1024)
}
}
func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
for {
expensive()
}
}
Chạy chương trình, sau đó dùng
go tool pprof http://localhost:6060/debug/pprof/heap
.Kết quả sẽ chỉ ra
expensive
cấp phát quá nhiều bộ nhớ.
Best Practices:
Dùng
context
để kiểm soát goroutine.Kiểm tra capacity của slice khi cắt:
s = s[:0:0]
để reset.Tích hợp
pprof
vào CI/CD để phát hiện leak sớm.
Kết luận
Pointer: Hiểu escape analysis và tối ưu cấp phát.
GC: Tối ưu qua
sync.Pool
và điều chỉnhGOGC
.Profiling: Dùng
pprof
để debug chuyên sâu, kết hợp context để tránh leak.
Last updated