⚡ 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ật | Ví Dụ Nhà Hàng | Lợi Ích |
|---|---|---|
| Profiling | Đo thời gian nấu từng món | Tìm bottleneck |
| Worker Pool | Đội bếp cố định 5 người | Kiểm soát tài nguyên |
| Preallocate | Chuẩn bị đủ nguyên liệu trước | Giảm memory allocation |
| sync.Pool | Tái sử dụng đĩa | Giảm GC pressure |
| strings.Builder | Ghé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!