DEV Community

Pallat Anchaleechamaikorn
Pallat Anchaleechamaikorn

Posted on

อย่าเพิ่งใช้ fiber ถ้ายังไม่ได้อ่าน doc

เนื่องจากก่อนหน้าที่ผมจะเคยได้ใช้ 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))
Enter fullscreen mode Exit fullscreen mode

เราจะทดลองประกาศตัวแปร s และกำหนดค่าเป็น "Hello" ไว้ แล้วเรามาลอง print address มันดู โดยเราจะพิมพ์ออกมาจาก &s ตัวหนึ่ง และใช้ unsafe.StringData มาช่วยอีกตัวหนึ่ง จากนั้นเราเพิ่มคำให้กับ s แล้วลองพิมพ์ address ออกมาอีกรอบ

14000028250 100b140fc
14000028250 1400000e190
Enter fullscreen mode Exit fullscreen mode

ผลลัพธ์ก์จะประมาณนี้ โดยที่ &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'
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

เวลารัน 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
Enter fullscreen mode Exit fullscreen mode

เราจะเห็นที่ 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)

Collapse
 
zerocode profile image
DevGodFather

ขอบคุณครับบบบ ชอบอ่านอะไรแบบนี้มาก ติดตามครับบบ

Collapse
 
panudetjt profile image
Panudet Tammawongsa • Edited

ตรง function Immutable() I/O ของ print จะถูก benchmark ไปด้วยหรือเปล่าครับ ?

ขอบคุณครับ

Collapse
 
pallat profile image
Pallat Anchaleechamaikorn

นั่น ผมลืมเอาออก 🙏😅

Collapse
 
panudetjt profile image
Panudet Tammawongsa

อ่อครับ ขอบคุณครับ

Collapse
 
pallat profile image
Pallat Anchaleechamaikorn

ผมทดสอบโดยไม่มี fmt.Print ครับ พอดีลองโน่นนี่แล้วก็เลยลืมเอาออก
ผลที่ได้ตามนี้เลยครับ 🙏 ขอบคุณอีกครับนะครับ

Collapse
 
boonsanti profile image
boonsanti

ขอบคุณครับอาจารย์ยอด 🙏😊