เนื่องจากก่อนหน้าที่ผมจะเคยได้ใช้ fiber ผมก็ลองใช้มาหลาย framework เช่น echo หรือ gin ซึ่งที่จริงก็ลองมาหลายตัว แต่จะใช้จริงจังอยู่ 2 ตัวนี้เป็นหลัก แต่หลังๆมาช่วง 2ปีมานี้ ผมเริ่มได้ยินว่ามีคนเริ่มใช้ fiber กันเยอะ ซึ่งแรกๆผมก็เข้าใจว่า ก็คงไม่ได้ต่างอะไรกับ framework อื่นๆมากนัก จนกระทั่งได้ลองไปอ่าน docs มันดู ก็ได้สะดุดกับหัวข้อนึงที่เขาอธิบายเรื่อง Zero Allocation
แล้วก็มีประโยคนี้
Because fiber is optimized for high-performance, values returned from fiber.Ctx are not immutable by default and will be re-used across requests
ด้วยควาสงสัยก็เลยลองไปค้นข้อมูลดูก็พบว่าสิ่งนี้เคยมีคนไปเปิด issue ไว้เมื่อปี 2020 https://github.com/gofiber/fiber/issues/426
ความจริงมันไม่น่าจะเป็นปัญหา ถ้าเราทุกคนอ่าน document กันก่อน🤭
ผมยอมรับตรงๆว่า ผมก็ไม่ได้เอะใจว่าจะต้องมาอ่าน doc ก่อน เพราะคิดเอาเองว่ามันก็เหมือนๆกันนั่นแหล่ะ ทีนี้ก็เลยต้องมาอธิบายเรื่องที่เขาบอกว่าไอ้เจ้า fiber.Ctx มันไม่ immutable by default
ว่ามันหมายความว่าอะไร
ซึ่งก็มีคนอธิบายเรื่องนี้ิอยู่บ้างเช่น https://www.meetgor.com/golang-mutable-immutable/
ทีนี้ถ้าจะให้สรุปสั้นๆง่ายๆก็จะประมาณว่า ถ้า data type ใดที่เราสามารถเปลี่ยนแปลงค่ามันได้ โดยไม่กระกบต่อการ allocate memory เลย มันคือ mutable ในทางกลับกัน ถ้าเราจะเปลี่ยนแปลงค่าใน data type แล้วมันต้องไปทำการ allocate memory ใหม่ มันก็จะเป็น immutable
ยกตัวอย่าง string ใน Go ก็เป็น immutable เช่นกัน ทุกครั้งที่เราแก้ไขค่าให้ตัวแปร string มันจะไป allocate mem ที่ใหม่เสมอ ลองดู code ชุดนี้
s := "Hello"
fmt.Printf("%x %x\n", &s, unsafe.StringData(s))
s += " World"
fmt.Printf("%x %x\n", &s, unsafe.StringData(s))
เราจะทดลองประกาศตัวแปร s และกำหนดค่าเป็น "Hello" ไว้ แล้วเรามาลอง print address มันดู โดยเราจะพิมพ์ออกมาจาก &s ตัวหนึ่ง และใช้ unsafe.StringData
มาช่วยอีกตัวหนึ่ง จากนั้นเราเพิ่มคำให้กับ s
แล้วลองพิมพ์ address ออกมาอีกรอบ
14000028250 100b140fc
14000028250 1400000e190
ผลลัพธ์ก์จะประมาณนี้ โดยที่ &s
จะได้ address เดิมออกมาทั้งสองครั้ง แต่พอใช้ unsafe.StringData
มันจะได้ address ที่ไม่เหมือนกัน
เพราะว่า ที่จริงแล้ว string ใน Go มันคือ pointer ที่อ้างถึง array ของ byte โดย Russ Cox เคยเขียนอธิบายไว้ที่นี่ https://research.swtch.com/godata
นั่นทำให้เมื่อเราพิมพ์ &s
ออกมา เราก็จะได้ address ของ s ซึ่งไม่ใช่ address ของข้อความจริงๆที่มันอ้างถึง และถ้าเราอยากจะรู้ address จริงๆของข้อความ เราก็เลยต้องใช้ unsafe.StringData
มาช่วย โดย StringData
จะคืน *byte ที่เป็น underlaying ของ s ออกมา
หรือถ้าต้องการทดสอบว่ามันมีการ allocate จริงๆ ก็ลองเขียน Benchmark เล่นๆดูแบบนี้
immutable.go
func Immutable() {
var s string
for i := 0; i < 1000; i++ {
s += "a"
}
}
func Mutable() {
var b [1000]byte
for i := 0; i < 1000; i++ {
b[i] = 'a'
}
}
immutable_test.go
func BenchmarkImmutable(b *testing.B) {
for i := 0; i < b.N; i++ {
Immutable()
}
b.ReportAllocs()
}
func BenchmarkMutable(b *testing.B) {
for i := 0; i < b.N; i++ {
Mutable()
}
b.ReportAllocs()
}
เวลารัน benchmark ให้ใช้คำสั่ง
go test -bench=.
ทีนี้มันก็จะ report ออกมาประมาณนี้ (ข้อความยาว อาจจะต้อง scroll ไปทางขวา เพื่อดู)
BenchmarkImmutable-12 18628 57846 ns/op 530277 B/op 999 allocs/op
BenchmarkMutable-12 4029364 299.0 ns/op 0 B/op 0 allocs/op
เราจะเห็นที่ column B/op
ว่าตอนที่เราใช้ string แล้วเพิ่มข้อความไปเรื่อยๆ มันจะเกิดการ allocate พื้นที่ใหม่ไป 999 ครั้งในแต่ละรอบ ตามจำนวนที่เราเปลี่ยนข้อความใน Immutable 1000 รอบ โดยในรอบแรกจะไม่ allocate
และพอเรามาเขียนด้วยการใช้ array ของ byte ใน mutable
จะเห็นว่ามันไม่จำเป็นต้อง allocate เลย เนื่องจาก array เป็น mutable data type แล้วพอไปดูที่ความเร็วที่มันทำได้ เราก็จะเห็นว่า เมื่อเราใช้ mutable data type ความเร็วมันจะเร็วกว่ามาก(ดูที่ค่า ns/op) นั่นคือเหตุผลว่าทำไม fiber ถึงทำความเร็วได้ดีกว่า framework อื่น
แต่มันก็ต้องมีสิ่งที่ต้องแลกถ้าเราอยากได้ความเร็ว คือ ถ้าเราเผลอเอาค่าจาก fiber.Ctx ไปใช้นอก handler โดยเฉพาะใน Goroutine ทดสอบง่ายๆด้วยการเขียนตาม issue ที่เขาเปิดไว้(สามารถทดลองได้จากที่นี่ https://github.com/pallat/fibertest) ซึ่ง fiber เองก็เขียนวิธีแก้เอาไว้แล้ว (อ่านใน docs)
หวังว่าข้อความนี้จะเป็นประโยชน์ในการเลือกใช้ framework นะครับ
Top comments (6)
ขอบคุณครับบบบ ชอบอ่านอะไรแบบนี้มาก ติดตามครับบบ
ตรง function Immutable() I/O ของ print จะถูก benchmark ไปด้วยหรือเปล่าครับ ?
ขอบคุณครับ
นั่น ผมลืมเอาออก 🙏😅
อ่อครับ ขอบคุณครับ
ผมทดสอบโดยไม่มี fmt.Print ครับ พอดีลองโน่นนี่แล้วก็เลยลืมเอาออก
ผลที่ได้ตามนี้เลยครับ 🙏 ขอบคุณอีกครับนะครับ
ขอบคุณครับอาจารย์ยอด 🙏😊