+7

Golang: Top 5 Cách Để Tăng Performance

Claims

Bài viết được dịch từ bài viết của mình: Go Performance Boosters: The Top 5 Tips and Tricks You Need to Know

1. Tránh sử dụng reflection

Reflection là một tính năng mạnh mẽ của Go và các ngôn ngữ lập trình khác, cho phép chúng ta phân tích và sửa đổi cấu trúc của kiểu (type) cũng như hành vi của nó tại runtime.

Bạn có thể sử dụng reflection để xác định kiểu của một giá trị, truy cập các trường của nó và gọi các phương thức của nó như sau:

package main  
  
import (  
  "fmt"  
  "reflect"  
)  
  
func main() {  
  x := 100  
  v := reflect.ValueOf(x)  
  t := v.Type()  
  fmt.Println("Type:", t) // "Type: int"  
}

Tuy nhiên, khi sử dụng reflection, nó liên quan đến việc phân tích và thao tác với các giá trị trong thời gian chạy (runtime), thay vì tại thời điểm biên dịch (compile time).

Hệ thống runtime của Go phải thực hiện thêm công việc để xác định kiểu và cấu trúc của giá trị được reflect thay vì compile, điều này có thể tăng thêm workloads cho go-runtime và làm chậm chương trình.

"Tại sao runtime lại thêm việc?"

Đơn cử cách chúng ta sử dụng biến, nếu các biến đã được define trước như hằng số (const - compile time) chẳng hạn, so với các biến động (dynamic - runtime), thì tất nhiên hằng số luôn có performance tốt hơn.

Reflection cũng có thể khiến code khó đọc và hiểu hơn, điều này có thể ảnh hưởng đến hiệu suất làm việc của team.

2. Tránh nối chuỗi bằng +

Để nối 2 chuỗi, thường hiệu quả hơn nếu chúng ta sử dụng kiểu bytes.Buffer thay vì nối chuỗi bằng toán tử +.

Trước tiên, chúng ta hãy xem xét đoạn code sau:

s := ""  
for i := 0; i < 100000; i++ {  
  s += "x"  
}  
fmt.Println(s)

Đoạn code này sẽ tạo ra 3 chuỗi mới trên mỗi vòng lặp bao gồm s, s+xx bởi string là immutable type. Điều này có thể không hiệu quả và có thể dẫn đến performance kém.

Thay vào đó, bạn có thể sử dụng bytes.Buffer để xây dựng chuỗi một cách "performance" hơn, bởi buffer là mutable type và có thể thay đổi tuỳ ý:

var buffer bytes.Buffer  
  
for i := 0; i < 100000; i++ {  
  buffer.WriteString("x")  
}  
  
s := buffer.String()  
fmt.Println(s)

Dưới đây là một giải pháp khác: sử dụng strings.Builder. Cách sử dụng của nó tương tự như bytes.Buffer, nhưng nó cung cấp hiệu suất tốt hơn:

var builder strings.Builder  
  
for i := 0; i < 100000; i++ {  
  builder.WriteString("x")  
}  
  
s := builder.String()  
fmt.Println(s)

“Có benchmark hay gì không?”

Mình đã so sánh 2 cách này và kết quả:

  • Sử dụng bytes.Buffer nhanh hơn đáng kể so với việc sử dụng nối chuỗi bằng +, cải thiện performance lên đến > 250x trong một số trường hợp.
  • Sử dụng strings.Builder nhanh hơn khoảng 1.5 lần so với bytes.Buffer

Và cần lưu ý rằng mức độ tăng performance chính xác có thể thay đổi tùy thuộc vào các yếu tố như CPU gì? ngữ cảnh chạy của code? máy của bạn đang xử lý tác vụ nào...

“Tại sao strings.Builder nhanh hơn bytes.Buffer?”

Điều này là bởi vì strings.Builder được tối ưu hóa cho việc xây dựng chuỗi. Ngược lại, bytes.Buffer là một bộ đệm (buffer) được design cho mục đích chung có thể được sử dụng để xây dựng bất kỳ loại dữ liệu nào, nhưng có thể không được tối ưu hóa cho việc xây dựng chuỗi như strings.Builder.

3. Cấp phát trước cho slice, map

Cấp phát một slice với dung lượng phù hợp với số lượng phần tử mà nó dự kiến sẽ tăng performance đáng kể cho phần mềm, đặc biệt là trong Go. Đồng thời thì đây là một trong những tips cải thiện hiệu suất khá tốt mà mình ít thấy nhiều người sử dụng, bởi khó trong việc ước tính số lượng phần tử cho slice.

Performance tăng bởi việc cấp phát một slice với dung lượng lớn hơn có thể giảm số lần thay đổi kích thước của slice khi thêm phần tử.

Dưới đây là benchmark đơn giản:

func main() {  
  // Allocate a slice with a small capacity  
  start := time.Now()  
  s := make(\[\]int, 0, 10)  
  for i := 0; i < 100000; i++ {  
    s = append(s, i)  
  }  
  elapsed := time.Since(start)  
  fmt.Printf("Allocating slice with small capacity: %v\\n", elapsed) // 1.165208ms  
    
  // Allocate a slice with a larger capacity  
  start = time.Now()  
  s = make(\[\]int, 0, 100000)  
  for i := 0; i < 100000; i++ {  
    s = append(s, i)  
  }  
  elapsed = time.Since(start)  
  fmt.Printf("Allocating slice with larger capacity: %v\\n", elapsed) // 361.333µs  
}

Vâng, chúng ta đã có thể tăng performance x3 với việc cấp phát trước.

Nếu bạn muốn hiểu tại sao việc cấp phát trước nhanh hơn, mình đã viết một bài giải thích chi tiết trong một bài viết về slices: why pre-allocation is faster.

4. Tránh sử dụng interfaces với một kiểu cụ thể duy nhất

Nếu bạn biết rằng một interface chỉ có một loại implement duy nhất, bạn có thể sử dụng loại cụ thể trực tiếp để tránh gánh nặng do interface.

Việc sử dụng trực tiếp kiểu cụ thể có thể hiệu quả hơn việc sử dụng interface vì nó tránh việc lưu trữ kiểu và giá trị trong interface.

Dưới đây là một ví dụ so sánh hiệu suất giữa việc sử dụng interface và sử dụng trực tiếp kiểu cụ thể trong Go:

type Shape interface {  
  Area() float64  
}  
  
type Circle struct {  
  radius float64  
}  
  
func (c \*Circle) Area() float64 {  
  return 3.14 \* c.radius \* c.radius  
}  
  
func main() {  
  // Use the Shape interface  
  start := time.Now()  
  var s Shape = &Circle{radius: 10}  
  for i := 0; i < 100000; i++ {  
    s.Area()  
  }  
  elapsed := time.Since(start)  
  fmt.Printf("Using Shape interface: %s\\n", elapsed) // Using Shape interface: 358µs  
    
  // Use the Circle type directly  
  start = time.Now()  
  c := Circle{radius: 10}  
  for i := 0; i < 100000; i++ {  
    c.Area()  
  }  
  elapsed = time.Since(start)  
  fmt.Printf("Using Circle type directly: %s\\n", elapsed) // Using Circle type directly: 341.917µs  
}

Sử dụng interface mất 358μs, kiểu cụ thể mất 342μs.

Cần lưu ý rằng kỹ thuật này chỉ nên được sử dụng khi bạn chắc chắn rằng một interface chỉ bao giờ có một kiểu cụ thể duy nhất (1 type implement interface)

5. Sử dụng govet

govet là một công cụ phân tích tĩnh mà không cần chạy code, nó vẫn có thể giúp bạn tìm ra những vấn đề tiềm ẩn trong code của bạn.

Để sử dụng govet, bạn có thể chạy lệnh go tool vet và truyền tên của các tệp nguồn Go mà bạn muốn kiểm tra làm đối số:

go tool vet main.go

Bạn cũng có thể truyền flag -all vào go tool vet để kiểm tra tất cả các tệp nguồn Go trong thư mục hiện tại và các thư mục con của nó:

go tool vet -all

““Govet quá spam. Một số lỗi không cần phải report.”

Bạn có thể tùy chỉnh hành vi của govet bằng cách viết "vet comments" trong mã của bạn. vet comment là những comment đặc biệt để báo cho govet biết phải bỏ qua những vấn đề nhất định.

Dưới đây là một ví dụ về một vet comment cho biết govet không chú ý đến một biến không sử dụng:

func main() {  
 var x int  
 //go:noinline  
 _ = x  
}

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí