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 (&*) - 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ặc GOGC=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

Nguyê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ỉnh GOGC.

  • Profiling: Dùng pprof để debug chuyên sâu, kết hợp context để tránh leak.

Last updated