optimize-golang-with-memoization preview

เพิ่มประสิทธิภาพโปรแกรมภาษา Go ด้วยเทคนิก Memoization

Memoization เป็นเทคนิกในการเพิ่มประสิทธิภาพสำหรับโปรแกรมที่มี cost ในการคำนวนสูงๆ โดยการเก็บผลลัพธ์ที่เคยคำนวนไว้ และคืนกลับไปหากได้รับ input parameters เดิม ซึ่งจะช่วยลด expensive calculation ไปได้มหาศาล

ในการเขียนโปรแกรม บางครั้งเราต้องเขียนโปรแกรมเพื่อคำนวนอะไรบางอย่างที่มีการคำนวนหนักๆ และใช้พลังงานเครื่องเยอะ เช่น ฟังก์ชั่นคำนวนเลข Fibonacci, การคำนวน Factorial เป็นต้น ซึ่งฟังก์ชั่นพวกนี้มักจะมีการวนการทำงาน หรือบางครั้งถูกเขียนอยู่ในรูปของ Recursive Function แน่นอนว่าต้องใช้ทรัพยากรสูงขึ้นเรื่อยๆ ตามจำนวน function calls หรือรอบของการทำงาน

ตัวอย่างโปรแกรมคำนวน Fibonacci

package main

func fibonacci(n int) int {
    if n <= 1 {
        return n
    }
    return fibonacci(n-1) + fibonacci(n-2)
}


func main() {
    result := fibonacci(20)
    fmt.Println(result) // the result is 6765
}

จากโปรแกรมดังกล่าว จะเห็นว่ามีการใช้เทคนิกการเขียนโปรแกรมอย่าง Recursive Function หรือก็คือการที่ฟังก์ชั่นนั้น เรียกฟังก์ชั่นตัวเองเพืื่อวนรอบการทำงาน ซึ่งหากค่าเริ่มต้นของโปรแกรมมีสูง Call stacks ก็จะยิ่งสูงมาขึ้น สิ้นเปลือง CPU, I/O หรือ Memory เป็นอย่างมาก

เรามาดูคะแนน Benchmark กัน ทดสอบด้วยชุดคำสั่งง่ายๆ

package main
import "testing"

func BenchmarkFibonacci(b *testing.B) {
    for i := 0; i < b.N; i++ {
        fibonacci(20)
    }
}

ผลการทดสอบ

goos: darwin
goarch: arm64
pkg: lab/go-fibonacci
=== RUN   BenchmarkFibonacci
BenchmarkFibonacci
BenchmarkFibonacci-8       47425             22578 ns/op               0 B/op          0 allocs/op
PASS
ok      lab/go-fibonacci        2.504s

ที่นี้ลองดูมาปรับฟังก์ชั่น Fibonacci ให้เป็นแบบ memoization กันดู ได้หน้าตาแบบนี้

func fibonacciMemo(n int, memo map[int]int) int {
    if n <= 1 {
        return n
    }
    if val, ok := memo[n]; ok {
        return val
    }
    memo[n] = fibonacciMemo(n-1, memo) + fibonacciMemo(n-2, memo)
    return memo[n]
}

func main() {
    memo := make(map[int]int)
    result := fibonacciMemo(20, memo)
    fmt.Println(result)
}

สิ่งที่เพิ่มขึ้นมาก็คือ map[int]int สำหรับเก็บผลลัพธ์ของการคำนวน fibonacci n หากมีผลลัพธ์อยู่แล้ว ก็คืนค่าออกไปเลยไม่ต้องคำนวนใหม่ เทคนิก memoization จะช่วยทำให้ไม่ต้องคำนวน fibonacci ของ n ซ้ำใหม่ทุกครั้ง โดยเก็บผลลัพธ์ที่เคยคำนวนไว้ใน memory นั่นเอง

มาดูผล benchmark กัน

goos: darwin
goarch: arm64
pkg: lab/go-fibonacci
=== RUN   BenchmarkFibonacciOptimized
BenchmarkFibonacciOptimized
BenchmarkFibonacciOptimized-8            1319659               882.5 ns/op           932 B/op          3 allocs/op
PASS
ok      lab/go-fibonacci        3.070s

ผลลัพธ์ค่อนข้างน่าสนใจ โดยหากดูจาก execution time ที่ลดลงจาก 22578 ns/op เหลือเพียง 880 ns/op ก็ถือว่าทำเวลาลดลงมาได้ถึง 25 เท่า! แต่ก็จะมี trade-off ตรงที่มีการ allocation เพิ่มเติมขึ้นมา เพื่อทำการเก็บผลลัพธ์ไว้ใช้ซ้ำแหละ

ก่อนจากกัน เทคนิกการเขียนโปรแกรมแบบต่างๆ มันมีจุดประสงค์ของมัน บ้างเพื่อเพิ่มความเร็ว บ้างก็เพื่อลดการใช้ทรัพยากร ทั้งนี้ทั้งนั้น Developer ก็ควรจะต้องรู้จุดประสงค์ของสิ่งที่ตัวเองกำลังทำหรือต้องการจะทำ เข้าใจว่าทำไปทำไม ทำไปแล้วได้อะไร หรือ ตัดสินใจได้ว่าจะไม่ทำ เพราะอะไร

ขอให้มีความสุขกับการเขียนโปรแกรมครับ

← การเลือกใช้ฟอนต์สำหรับหน้าเว็บง่ายๆ แบบไม่คิดเยอะ Go Testing Part 2 - Write Testable Code →