1 คะแนน โดย GN⁺ 2025-04-24 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ภาษา Go แทบไม่มี พฤติกรรมที่ไม่ถูกกำหนดไว้ และมี เซแมนติกของ GC (garbage collection) ที่เรียบง่าย
  • ใน Go สามารถทำ การจัดการหน่วยความจำแบบแมนนวล ได้ และทำได้โดยทำงานร่วมกับ GC
  • Arena คือโครงสร้างข้อมูลสำหรับจัดสรรหน่วยความจำอย่างมีประสิทธิภาพให้กับข้อมูลที่มีอายุการใช้งานเท่ากัน และบทความนี้อธิบายวิธีนำแนวคิดนี้มาใช้งานใน Go
  • อธิบายวิธีที่ GC จัดการหน่วยความจำผ่านอัลกอริทึม Mark and Sweep
  • สามารถใช้ Arena เพื่อปรับปรุง ประสิทธิภาพการจัดสรรหน่วยความจำ ได้ และทำได้ผ่านการปรับแต่งหลายแบบ
  • พยายามเพิ่มประสิทธิภาพและลดภาระของ GC ให้ต่ำที่สุดผ่าน การลบ write barrier, การนำหน่วยความจำกลับมาใช้ใหม่, chunk pooling เป็นต้น
  • นำเสนอแพตเทิร์นที่ปลอดภัยและรวดเร็วสำหรับการจัดการหน่วยความจำขนาดใหญ่จริง ผ่าน การทำ Realloc, การนำ Arena กลับมาใช้ใหม่ และการรีเซ็ต (Reset)

ภาพรวมของการจัดสรรหน่วยความจำแบบแมนนวลด้วย Arena ใน Go

  • Go เป็นภาษาที่ปลอดภัยด้วย การทำงานของ GC ที่ชัดเจน และ Undefined Behavior ที่แทบไม่มี
  • สามารถใช้แพ็กเกจ unsafe เพื่อ ควบคุมหน่วยความจำโดยตรงให้สอดคล้องกับการทำงานภายในของ GC
  • บทความนี้อธิบายวิธีสร้างตัวจัดสรรหน่วยความจำแบบ Arena structure ที่สามารถทำงานร่วมกับ GC ได้ใน Go

ความหมายของ Arena และเหตุผลที่ต้องใช้

  • Arena คือโครงสร้างสำหรับ จัดสรรอ็อบเจ็กต์ที่มีอายุการใช้งานเท่ากันอย่างมีประสิทธิภาพ
  • หาก append แบบทั่วไปใช้วิธี ขยายอาร์เรย์แบบเอ็กซ์โปเนนเชียล Arena จะใช้วิธี เพิ่มบล็อกใหม่และส่งพอยน์เตอร์กลับไป
  • อินเทอร์เฟซมาตรฐานมีดังนี้:
    • Alloc(size, align uintptr) unsafe.Pointer

พอยน์เตอร์และวิธีการทำงานของ GC

  • GC ทำงานด้วยวิธี ติดตามหน่วยความจำ (mark) และกวาดคืน (sweep)
  • เพื่อให้เป็น GC แบบแม่นยำ จะใช้เมทาดาทาชื่อ pointer bits เพื่อบอกตำแหน่งของพอยน์เตอร์
  • หากจัดการพอยน์เตอร์ใน Arena ผิดพลาด GC อาจติดตามพอยน์เตอร์ไม่เจอ และอาจเกิด ข้อผิดพลาดแบบ Use-After-Free ได้

วิธีออกแบบ Arena

  • โครงสร้าง Arena มีฟิลด์ดังนี้:
    • next, left, cap, chunks
  • การจัดสรรทั้งหมดจะจัดแนวที่ 8 ไบต์ และถ้าไม่พอจะสร้าง chunk ใหม่ด้วย nextPow2
  • chunk จะถูกจัดสรรเป็นชนิด struct { A [N]uintptr; P *Arena } แทน []uintptr เพื่อให้ GC สามารถติดตาม Arena ได้

วิธีทำให้ Arena ปลอดภัยต่อพอยน์เตอร์

  • หากใช้พอยน์เตอร์ที่จัดสรรจากภายใน Arena เท่านั้น GC จะคงทั้ง Arena เอาไว้
  • ตั้งค่าให้พอยน์เตอร์อ้างอิง Arena เพื่อ รับประกันว่า Arena ทั้งก้อนจะรอดจาก GC
  • เมธอดจัดสรรของ Arena จะทำสิ่งต่อไปนี้:
    • เก็บพอยน์เตอร์ของ Arena ไว้ที่ท้าย chunk ใน allocChunk()

ผลลัพธ์เบนช์มาร์กด้านประสิทธิภาพ

  • เมื่อเทียบกับ new ปกติ การจัดสรรด้วย Arena ให้ประสิทธิภาพดีขึ้นเฉลี่ย มากกว่า 2~4 เท่า
  • แม้ในสถานการณ์ที่มีภาระ GC สูง วิธีแบบ Arena ก็ยังให้ประสิทธิภาพเหนือกว่าถึง มากกว่า 2 เท่า
  • การปรับแต่งอย่างการลบ write barrier และการใช้ uintptr ช่วยเพิ่มประสิทธิภาพ สูงสุด 20% ในการจัดสรรขนาดเล็ก

กลยุทธ์การนำ Chunk กลับมาใช้ใหม่และการลดการใช้ Heap

  • สามารถนำ chunk กลับมาใช้ใหม่ได้ด้วย sync.Pool
  • ใช้ runtime.SetFinalizer() เพื่อคืน chunk กลับเข้าพูลเมื่อ Arena ถูกทำลาย
  • ประสิทธิภาพจะดีมากในการจัดสรรขนาดเล็ก แต่ในการจัดสรรขนาดใหญ่ อาจช้ากว่า new ได้

ฟังก์ชันการรีเซ็ตและนำ Arena กลับมาใช้ใหม่

  • สามารถทำให้ Arena กลับไปยังสถานะเริ่มต้นได้ด้วยเมธอด Reset()
  • แม้จะมีความเสี่ยงสูง แต่สามารถนำโครงสร้างเดิมกลับมาใช้ใหม่ได้โดยไม่ต้องจัดสรรหน่วยความจำใหม่
  • แม้ตอนนำกลับมาใช้ใหม่ ก็ยังใช้ chunk เดิมซ้ำได้ ทำให้ประสิทธิภาพดีขึ้นอย่างมาก

การทำฟังก์ชัน Realloc

  • สามารถทำฟังก์ชัน realloc ใน Arena เพื่อ ขยายหน่วยความจำแบบไดนามิกสำหรับการจัดสรรล่าสุด ได้
  • หากทำไม่ได้ จะจัดสรรหน่วยความจำใหม่แล้วคัดลอกข้อมูลแทน

บทสรุปและโค้ดฉบับเต็ม

  • บทความนี้ทำตัวจัดการหน่วยความจำแบบ Arena-based ให้สมบูรณ์ โดยอาศัยความเข้าใจเชิงลึกเกี่ยวกับกลไก GC ของ Go และการทำงานภายใน
  • เป็นโครงสร้างที่ได้ทั้งความปลอดภัยและประสิทธิภาพ และหากใช้อย่างเหมาะสมจะมีประโยชน์มากกับการจัดการโครงสร้างข้อมูลขนาดใหญ่
  • โค้ดฉบับเต็มประกอบด้วยโครงสร้าง Arena และ New, Alloc, Reset, allocChunk, finalize เป็นต้น

1 ความคิดเห็น

 
GN⁺ 2025-04-24
ความคิดเห็นบน Hacker News
  • บทความนี้อ่านสนุกดี

    • ถ้าคุณชอบบทความนี้หรืออยากควบคุมการจัดสรรหน่วยความจำใน Go ได้ดีขึ้น ลองดูแพ็กเกจที่ฉันเขียนไว้
    • ยินดีรับฟีดแบ็กหรือให้คนอื่นลองนำไปใช้
    • แพ็กเกจนี้จัดสรรหน่วยความจำของตัวเองแยกจากรันไทม์ จึงข้าม GC ไปได้ทั้งหมด
    • ตอนจัดสรรจะไม่ยอมรับชนิด pointer แต่ใช้แทนด้วยชนิด Reference[T] ที่ให้ความสามารถแบบเดียวกัน
    • การคืนหน่วยความจำทำด้วยมือ จึงพึ่งพา garbage collection ไม่ได้
    • ตัวจัดสรรแบบคัสตอมใน Go ลักษณะนี้โดยทั่วไปมักมุ่งไปที่ arena ที่รองรับกลุ่มการจัดสรรซึ่งถูกสร้างและทำลายพร้อมกัน
    • แต่แพ็กเกจ offheap มีเป้าหมายเพื่อสร้างโครงสร้างข้อมูลขนาดใหญ่ที่อยู่ระยะยาวโดยให้ต้นทุน garbage collection เป็นศูนย์
    • เช่น in-memory cache หรือฐานข้อมูลขนาดใหญ่
  • ช่วงหลังฉันปรับจูนประสิทธิภาพใน Go และลงเอยด้วยการใช้ดีไซน์ arena ที่คล้ายกันมากเพื่อรีดประสิทธิภาพให้สุด

    • ใช้ byte slice เป็น buf และ chunk แทน unsafe pointer
    • ลองทำแบบนั้นแล้วแต่ไม่ได้เร็วขึ้น และซับซ้อนกว่ามาก
    • ต้องกลับไปตรวจอีกทีให้แน่ใจก่อนจะมั่นใจ 100%
  • จุดปรับปรุงง่าย ๆ บางอย่าง

    • ถ้าเริ่มจาก slice เล็ก ๆ และมีบาง payload ถูกเพิ่มเข้ามาจำนวนมาก ให้เขียน append ของตัวเองที่เพิ่ม cap แบบดุดันกว่านี้ก่อนเรียก append ในตัว
    • unsafe.String มีประโยชน์สำหรับส่งต่อสตริงจาก byte slice โดยไม่ต้องมีการจัดสรร
    • ต้องอ่านคำเตือนให้ละเอียดและเข้าใจว่ามันทำอะไร
  • ไม่ค่อยเกี่ยวกับประเด็นหลัก แต่ฉันชอบ minimap ที่อยู่ด้านข้าง

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

    • OP สร้าง arena allocator ใน Go โดยใช้ unsafe เพื่อเร่งงานด้าน allocator
    • มีประโยชน์เป็นพิเศษเมื่อจัดสรรสิ่งจำนวนมากที่ถูกสร้างและทำลายพร้อมกัน
    • ปัญหาหลักคือ GC ของ Go ต้องรู้ layout ของข้อมูล (โดยเฉพาะตำแหน่งของ pointer) เพื่อให้ทำงานได้ถูกต้อง
    • ถ้าจัดสรร raw bytes ด้วย unsafe.Pointer GC จะมองไม่เห็นสิ่งที่ถูกอ้างถึงจากใน arena อย่างถูกต้องและอาจปล่อยทิ้งโดยไม่ตั้งใจ
    • แต่เพื่อให้มันยังทำงานได้ตราบใดที่ pointer ยังชี้ไปยังอย่างอื่นภายใน arena เดียวกัน ก็จะเก็บทั้ง arena ไว้หากยังมีบางส่วนของ arena ถูกอ้างอิงอยู่
    • (1) เก็บ slice (chunk) ที่ชี้ไปยังบล็อกหน่วยความจำขนาดใหญ่ทั้งหมดที่ arena ได้มาจากระบบ และ
    • (2) ใช้ reflect.StructOf เพื่อสร้างชนิดใหม่ที่มีฟิลด์ pointer เพิ่มเติมไปยังบล็อกเหล่านี้
    • ดังนั้นถ้า GC หา pointer ไปยัง chunk เจอ มันก็จะเจอ back pointer ด้วย ทำให้มาร์ก arena ว่ายังมีชีวิตอยู่และคง slice ของ chunk ไว้
    • จากนั้นยังแนะนำเทคนิคการปรับให้เหมาะสมที่น่าสนใจเพื่อเอาการตรวจสอบภายในหลายอย่างและ write barrier ออกไป
  • ที่เกี่ยวข้อง: การถกเถียงเรื่องการเพิ่ม "memory regions" เข้าไปใน standard library

    • ข้อเสนอ arena ก่อนหน้านี้
  • เนื้อหาน่าสนใจ

    • อยากรู้ว่าคนที่สร้างตัวจัดสรรแบบ off-heap หรือ arena style ใน Go โดยทั่วไปทดสอบหรือทำเบนช์มาร์กเรื่องความปลอดภัยของหน่วยความจำและการทำงานร่วมกับ GC กันอย่างไร
  • Go ให้ความสำคัญกับการไม่ทำลาย ecosystem

    • ซึ่งทำให้สามารถสมมุติได้ว่ากฎของ Hyrum จะคุ้มครองพฤติกรรมที่สังเกตได้บางอย่างของรันไทม์
    • ถ้าข้ออ้างนี้ถูกต้อง Go ในฐานะภาษาก็เป็นทางตันด้านวิวัฒนาการ
    • ในกรณีนั้นฉันก็ไม่แน่ใจว่า Go ยังน่าสนใจหรือไม่
  • หมายเหตุเชิงเมตาสั้น ๆ

    • บทความนี้ยาวมากจนฉันไม่มีเวลาอ่านรายละเอียดพื้นหลัง
    • ตัวอย่างเช่น ส่วน "Mark and Sweep" กินพื้นที่หน้าจอโน้ตบุ๊กฉันเกิน 4 หน้า
    • ส่วนนั้นเริ่มหลังจากผ่านไปมากกว่า 5 หน้าจากจุดเริ่มต้นของบทความ
    • สงสัยว่าเป็นผลจากการใช้ AI ช่วยเขียนแต่ละส่วนจนมันครอบคลุมมากเกินไปหรือเปล่า
    • การสร้างเนื้อหานั้นง่าย แต่ไม่มีการตัดสินใจเชิงบรรณาธิการเพื่อคงไว้เฉพาะส่วนสำคัญ
    • ฉันอยากรู้แค่ส่วนที่เกี่ยวกับ arena allocator ไม่ได้ต้องการบทสอนเรื่อง garbage collection