Skip to main content

⚡ Tối Ưu Tốc Độ - Performance Optimization

🎯 Mục Tiêu Bài Học

Trong bài này, bạn sẽ học cách tối ưu hóa hiệu suất ứng dụng Go, giống như một nhà hàng cần tối ưu tốc độ phục vụ để khách hàng hài lòng.

Sau bài học này, bạn sẽ:

  • Hiểu cách đo lường hiệu suất (profiling)
  • Biết cách tối ưu bộ nhớ
  • Áp dụng best practices cho goroutines
  • Sử dụng công cụ pprof để phân tích

🏪 Khái Niệm Nhà Hàng

Performance là gì?

Performance giống như tốc độ phục vụ trong nhà hàng:

Nhà Hàng Tốt:
┌─────────────────────────┐
│ Đặt món → 2 phút │
│ Nấu ăn → 10 phút │
│ Phục vụ → 1 phút │
│ TỔNG: 13 phút ✓ │
└─────────────────────────┘

Nhà Hàng Chậm:
┌─────────────────────────┐
│ Đặt món → 5 phút │
│ Nấu ăn → 30 phút │
│ Phục vụ → 5 phút │
│ TỔNG: 40 phút ✗ │
└─────────────────────────┘

📊 Profiling - Đo Thời Gian Nấu

CPU Profiling

CPU Profiling giống như đo thời gian mỗi món ăn:

package main

import (
"fmt"
"os"
"runtime/pprof"
"time"
)

// Món ăn mất nhiều thời gian
func nauMonCham() {
for i := 0; i < 1000000; i++ {
_ = fmt.Sprintf("Món %d", i)
}
}

// Món ăn nhanh
func nauMonNhanh() {
for i := 0; i < 1000; i++ {
_ = fmt.Sprintf("Món %d", i)
}
}

func main() {
// Bắt đầu đo thời gian
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()

start := time.Now()

// Nấu các món
nauMonCham()
nauMonNhanh()

elapsed := time.Since(start)
fmt.Printf("Tổng thời gian: %s\n", elapsed)
}

Phân tích kết quả:

go run main.go
go tool pprof cpu.prof

# Trong pprof console:
(pprof) top
# Hiển thị các function tốn nhiều CPU nhất

Memory Profiling

Memory Profiling giống như kiểm tra nguyên liệu còn lại:

package main

import (
"fmt"
"os"
"runtime"
"runtime/pprof"
)

type MonAn struct {
Ten string
NguyenLieu []string
GhiChu string
}

// Cách lãng phí nguyên liệu
func cheBienLangPhi() []MonAn {
menu := make([]MonAn, 0) // Không định trước kích thước

for i := 0; i < 10000; i++ {
mon := MonAn{
Ten: fmt.Sprintf("Món %d", i),
NguyenLieu: []string{"Rau", "Thịt", "Gia vị"},
GhiChu: "Ghi chú dài dài dài...",
}
menu = append(menu, mon)
}

return menu
}

// Cách tiết kiệm nguyên liệu
func cheBienTietKiem() []MonAn {
menu := make([]MonAn, 0, 10000) // Định trước kích thước

for i := 0; i < 10000; i++ {
mon := MonAn{
Ten: fmt.Sprintf("Món %d", i),
NguyenLieu: []string{"Rau", "Thịt", "Gia vị"},
GhiChu: "Ghi chú ngắn",
}
menu = append(menu, mon)
}

return menu
}

func main() {
// Đo memory
f, _ := os.Create("mem.prof")

cheBienTietKiem()

runtime.GC() // Thu dọn rác
pprof.WriteHeapProfile(f)
f.Close()

fmt.Println("Memory profile saved to mem.prof")
}

🧵 Goroutine Pool - Đội Bếp Hiệu Quả

Vấn đề: Quá nhiều đầu bếp

package main

import (
"fmt"
"time"
)

// CÁCH SAI: Tạo quá nhiều goroutines
func phucVuKhongHieuQua(orders []string) {
for _, order := range orders {
go func(o string) {
time.Sleep(100 * time.Millisecond)
fmt.Printf("Phục vụ: %s\n", o)
}(order)
}

time.Sleep(2 * time.Second)
}

// Với 10,000 orders sẽ tạo 10,000 goroutines!

Giải pháp: Worker Pool Pattern

package main

import (
"fmt"
"sync"
"time"
)

type DonHang struct {
ID int
Ten string
}

// Worker pool - Đội bếp cố định
func workerPool(orders <-chan DonHang, results chan<- string, wg *sync.WaitGroup) {
defer wg.Done()

for order := range orders {
// Đầu bếp nấu món
time.Sleep(100 * time.Millisecond)
result := fmt.Sprintf("Đầu bếp đã hoàn thành: %s (ID: %d)",
order.Ten, order.ID)
results <- result
}
}

func phucVuHieuQua() {
soDauBep := 5 // 5 đầu bếp
soDonHang := 20

orders := make(chan DonHang, soDonHang)
results := make(chan string, soDonHang)
var wg sync.WaitGroup

// Khởi tạo đội bếp
for i := 0; i < soDauBep; i++ {
wg.Add(1)
go workerPool(orders, results, &wg)
}

// Gửi đơn hàng
go func() {
for i := 1; i <= soDonHang; i++ {
orders <- DonHang{
ID: i,
Ten: fmt.Sprintf("Món %d", i),
}
}
close(orders)
}()

// Đóng results khi tất cả workers xong
go func() {
wg.Wait()
close(results)
}()

// Nhận kết quả
for result := range results {
fmt.Println(result)
}
}

func main() {
fmt.Println("=== Nhà hàng với Worker Pool ===")
start := time.Now()
phucVuHieuQua()
fmt.Printf("Thời gian: %s\n", time.Since(start))
}

💾 Tối Ưu Bộ Nhớ - Tiết Kiệm Nguyên Liệu

String Builder cho hiệu suất cao

package main

import (
"fmt"
"strings"
"time"
)

// CÁCH CHẬM: Nối string bằng +
func taoMenuCham(soMon int) string {
menu := ""
for i := 1; i <= soMon; i++ {
menu += fmt.Sprintf("Món %d: Phở\n", i)
}
return menu
}

// CÁCH NHANH: Dùng strings.Builder
func taoMenuNhanh(soMon int) string {
var builder strings.Builder
builder.Grow(soMon * 20) // Dự trữ bộ nhớ trước

for i := 1; i <= soMon; i++ {
builder.WriteString(fmt.Sprintf("Món %d: Phở\n", i))
}
return builder.String()
}

func main() {
soMon := 10000

// Test cách chậm
start := time.Now()
_ = taoMenuCham(soMon)
fmt.Printf("Cách chậm: %s\n", time.Since(start))

// Test cách nhanh
start = time.Now()
_ = taoMenuNhanh(soMon)
fmt.Printf("Cách nhanh: %s\n", time.Since(start))
}

Slice preallocation

package main

import (
"fmt"
"time"
)

type NguyenLieu struct {
Ten string
SoLuong int
}

// CÁCH CHẬM: Không định trước kích thước
func chuanBiCham() []NguyenLieu {
kho := make([]NguyenLieu, 0)

for i := 0; i < 100000; i++ {
kho = append(kho, NguyenLieu{
Ten: fmt.Sprintf("NL-%d", i),
SoLuong: i,
})
}
return kho
}

// CÁCH NHANH: Định trước kích thước
func chuanBiNhanh() []NguyenLieu {
kho := make([]NguyenLieu, 0, 100000)

for i := 0; i < 100000; i++ {
kho = append(kho, NguyenLieu{
Ten: fmt.Sprintf("NL-%d", i),
SoLuong: i,
})
}
return kho
}

func main() {
start := time.Now()
_ = chuanBiCham()
fmt.Printf("Không preallocate: %s\n", time.Since(start))

start = time.Now()
_ = chuanBiNhanh()
fmt.Printf("Có preallocate: %s\n", time.Since(start))
}

🔧 Benchmarking - So Sánh Hiệu Suất

Viết Benchmark Tests

// performance_test.go
package main

import (
"strings"
"testing"
)

// Benchmark nối string bằng +
func BenchmarkStringConcat(b *testing.B) {
for i := 0; i < b.N; i++ {
menu := ""
for j := 0; j < 100; j++ {
menu += "Món ăn "
}
}
}

// Benchmark dùng strings.Builder
func BenchmarkStringBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
var builder strings.Builder
for j := 0; j < 100; j++ {
builder.WriteString("Món ăn ")
}
_ = builder.String()
}
}

// Benchmark không preallocate
func BenchmarkSliceNoPrealloc(b *testing.B) {
for i := 0; i < b.N; i++ {
slice := make([]int, 0)
for j := 0; j < 1000; j++ {
slice = append(slice, j)
}
}
}

// Benchmark có preallocate
func BenchmarkSliceWithPrealloc(b *testing.B) {
for i := 0; i < b.N; i++ {
slice := make([]int, 0, 1000)
for j := 0; j < 1000; j++ {
slice = append(slice, j)
}
}
}

Chạy benchmark:

go test -bench=. -benchmem

# Kết quả mẫu:
# BenchmarkStringConcat-8 20000 85000 ns/op 500000 B/op 100 allocs/op
# BenchmarkStringBuilder-8 100000 15000 ns/op 10000 B/op 5 allocs/op

🎯 Best Practices

1. Tránh cấp phát bộ nhớ không cần thiết

// SAI
func layTenMon(mon string) string {
return strings.ToUpper(mon) // Tạo string mới
}

// ĐÚNG (nếu có thể)
func layTenMonHieuQua(mon []byte) {
for i := range mon {
if mon[i] >= 'a' && mon[i] <= 'z' {
mon[i] -= 32
}
}
}

2. Sử dụng sync.Pool để tái sử dụng objects

package main

import (
"fmt"
"sync"
)

type DiaAn struct {
MonAn string
Size int
}

var diaPool = sync.Pool{
New: func() interface{} {
return &DiaAn{}
},
}

func phucVuMonAn(ten string) {
// Lấy đĩa từ pool
dia := diaPool.Get().(*DiaAn)
dia.MonAn = ten
dia.Size = 10

// Sử dụng đĩa
fmt.Printf("Phục vụ %s trên đĩa\n", dia.MonAn)

// Trả đĩa về pool
dia.MonAn = ""
diaPool.Put(dia)
}

func main() {
for i := 1; i <= 5; i++ {
phucVuMonAn(fmt.Sprintf("Món %d", i))
}
}

3. Defer có chi phí - dùng có chọn lọc

package main

import (
"fmt"
"time"
)

// Có defer - chậm hơn một chút
func voidDefer() {
start := time.Now()
defer func() {
fmt.Println("Đóng bếp")
}()
// Làm việc
}

// Không defer - nhanh hơn
func khongDefer() {
start := time.Now()
// Làm việc
fmt.Println("Đóng bếp")
}

// Lưu ý: Chỉ tối ưu trong hot path

🔍 Công Cụ Profiling

1. pprof CPU Profile

package main

import (
"net/http"
_ "net/http/pprof"
)

func main() {
// Mở http://localhost:6060/debug/pprof/
go func() {
http.ListenAndServe("localhost:6060", nil)
}()

// Ứng dụng của bạn...
}

2. Trace

# Tạo trace file
go test -trace=trace.out

# Xem trace
go tool trace trace.out

📝 Bài Tập Thực Hành

Bài 1: Tối ưu xử lý menu

package main

import (
"fmt"
"strings"
)

// TODO: Tối ưu hàm này
func taoMenu(soMon int) string {
menu := ""
for i := 1; i <= soMon; i++ {
menu += fmt.Sprintf("%d. Phở Bò - 50,000đ\n", i)
}
return menu
}

func main() {
menu := taoMenu(1000)
fmt.Println(strings.Split(menu, "\n")[0])
}

Bài 2: Implement worker pool

package main

// TODO: Tạo worker pool xử lý 1000 đơn hàng
// với 10 workers

func main() {
// Code của bạn
}

🎓 Tóm Tắt

Kỹ ThuậtVí Dụ Nhà HàngLợi Ích
ProfilingĐo thời gian nấu từng mónTìm bottleneck
Worker PoolĐội bếp cố định 5 ngườiKiểm soát tài nguyên
PreallocateChuẩn bị đủ nguyên liệu trướcGiảm memory allocation
sync.PoolTái sử dụng đĩaGiảm GC pressure
strings.BuilderGhép menu hiệu quảNhanh hơn string concat

🔗 Bài Tiếp Theo

Học cách bảo mật ứng dụng Go: Security →


Ghi nhớ: Performance optimization giống như điều hành nhà hàng - đo lường mọi thứ, tối ưu quy trình, và luôn tìm cách phục vụ nhanh hơn!