Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

10 ปีแห่งการรอคอย เจาะลึกสถาปัตยกรรม Allocator ของ Rust และก้าวต่อไปในปี 2026

📅 วันที่เผยแพร่: 2026-03-13

สำหรับนักพัฒนา Rust ในสาย System Programming หรือผู้ที่ต้องรีดประสิทธิภาพระบบในระดับลึก การรอคอย “Custom Allocators” ให้ใช้งานได้จริงบน Stable channel ถือเป็นมหากาพย์ที่ยาวนานมาก หากนับจากวันที่ RFC นี้ถูกเสนอขึ้นมาก็เกือบจะครบหนึ่งทศวรรษแล้ว แม้ผลสำรวจล่าสุดจะชี้ว่ามีผู้ใช้งานจำนวนมากต้องการฟีเจอร์นี้เพื่อปลดล็อกข้อจำกัดของระบบ แต่ทำไมแค่การสร้าง Trait สำหรับจองและคืนพื้นที่หน่วยความจำถึงกลายเป็นโจทย์สถาปัตยกรรมที่ Core Team ต้องถกเถียงกันมายาวนานขนาดนี้

ปรัชญา Zero-cost Abstraction และปัญหา State ของ Allocator

จุดเริ่มต้นของความซับซ้อนนี้มาจากความพยายามที่จะรักษาปรัชญา “Zero-cost Abstraction” ของ Rust ไว้ ในปัจจุบัน Allocator Trait บน Nightly ถูกออกแบบมาอย่างเรียบง่าย มีเพียงฟังก์ชันจอง (allocate) และฟังก์ชันคืน (deallocate) เมื่อเราใช้งานโครงสร้างข้อมูลอย่าง Vec ควบคู่กับ Global Allocator ซึ่งไม่มีการเก็บ State ใดๆ ตัวคอมไพล์เลอร์จะฉลาดพอที่จะยุบรวมโค้ด (Inline) ทำให้ขนาดของ Vec ยังคงมีแค่พอยน์เตอร์ ความยาว และความจุเท่าเดิมเป๊ะ

แต่ปัญหาจะเริ่มปรากฏเมื่อเราจำเป็นต้องเขียน Allocator ที่มีการเก็บ State ของตัวเอง ซึ่งจะส่งผลให้ขนาดของ Container อย่าง Vec หรือ Box ต้องบวมขึ้นเพื่อเก็บตัว Allocator นี้เอาไว้ใช้ในจังหวะที่ต้องคืนหน่วยความจำ

อุปสรรคของ Zero-Sized Allocations

อุปสรรคแรกที่ทำให้ฟีเจอร์นี้ยังไปไม่ถึงฝั่งฝันคือเรื่องการจองพื้นที่ขนาดศูนย์ไบต์ (Zero-Sized Allocations) ปัจจุบันตัวกำหนดโครงสร้างหน่วยความจำ หรือ Layout อนุญาตให้เราส่งค่าขนาดเป็นศูนย์ได้ ซึ่งในทางปฏิบัติไม่ได้มีการจองพื้นที่จริง และ Allocator มักจะคืนค่าพอยน์เตอร์ตัวเดิมกลับมาซ้ำๆ พฤติกรรมนี้เองที่สุ่มเสี่ยงต่อการทำลายตรรกะความเท่ากันของพอยน์เตอร์ และอาจเป็นช่องโหว่ที่นำไปสู่ Undefined Behavior ได้ในบางกรณี ทางออกที่วงในกำลังถกเถียงกันคือการเปลี่ยนไปใช้ NonZeroLayout เพื่อบังคับให้ทุกการจองต้องมีขนาดเสมอ ตัดปัญหาความคลุมเครือนี้ทิ้งไปตั้งแต่ระดับ Type System

ความต้องการ Context ในบริบทของ Kernel

แต่ความท้าทายไม่ได้หมดแค่นั้น หากเรามองไปที่กลุ่มผู้ใช้งานระดับฮาร์ดคอร์อย่างทีม Rust for Linux พวกเขาไม่สามารถรอให้ฟีเจอร์นี้เสร็จสมบูรณ์ได้ และต้องแยกไปสร้าง Allocator Trait ของตัวเอง เหตุผลสำคัญคือในระดับเคอร์เนล การจองหน่วยความจำไม่ได้อิงแค่ “ขนาด” แต่ต้องการ “บริบท” (Context) ด้วย เช่น ต้องระบุ Flag การทำงาน หรือเจาะจง NumaNode สิ่งนี้สะท้อนให้เห็นว่า Trait พื้นฐานของ Rust อาจจะยังยืดหยุ่นไม่พอ และอาจมีความจำเป็นต้องเพิ่ม Associated Type เพื่อเปิดทางให้ผู้เรียกสามารถส่งผ่าน Context เข้าไปได้

Bump Allocator และปัญหาการคืนหน่วยความจำ

นอกจากนี้ เมื่อเราพูดถึงกระบวนทัศน์การจัดการหน่วยความจำแบบพิเศษอย่าง “Bump Allocator” ซึ่งมีพฤติกรรมกวาดล้างคืนพื้นที่ทั้งหมดในรวดเดียว มันแทบไม่มีความจำเป็นต้องใช้ฟังก์ชัน deallocate สำหรับข้อมูลแต่ละชิ้นเลย แต่ด้วยดีไซน์ปัจจุบัน หากเราสร้าง Box จาก Bump Allocator ตัวกล่องก็ยังถูกบังคับให้ต้องจดจำ State ของ Allocator ไว้เพื่อรอจังหวะ Drop ทำให้สูญเสียพื้นที่หน่วยความจำไปอย่างเปล่าประโยชน์

แม้จะมีข้อเสนอให้จับหั่นแบ่ง Trait ออกเป็นส่วน Allocator และ Deallocator แต่ก็ต้องแลกมากับความยุ่งยากมหาศาลในการแปลง Type ข้ามไปมา หรือแย่ที่สุดคืออาจต้องยอมให้เกิด Runtime Panic ซึ่งเป็นสิ่งที่ขัดกับหัวใจของ Rust อย่างรุนแรง เช่นเดียวกับเรื่องของการจัดการ Error ที่ปัจจุบันเป็นเพียง Type ว่างๆ ที่ไม่มีข้อมูลอะไรเลย ทำให้การดึงข้อมูลบริบทกลับมาเพื่อทำ Error Recovery แทบจะเป็นไปไม่ได้ จนเกิดเป็นข้อเสนอที่อยากให้มี Associated Type สำหรับ Error โดยเฉพาะ

บอสใหญ่: Static vs Dynamic Dispatch

แต่สิ่งที่ถือเป็น “บอสใหญ่” ในเชิงสถาปัตยกรรมจริงๆ คือปมเรื่องรอยต่อระหว่าง Static และ Dynamic Dispatch ปัจจุบัน Rust เลือกใช้ Generics สำหรับรับชนิดของ Allocator (เช่น Vec<T, A>) ซึ่งจะทำให้เกิดกระบวนการ Monomorphization หรือการปั๊มโค้ดออกมาชุดใหม่ทุกครั้งที่เราเปลี่ยน Allocator ผลที่ตามมาคือขนาดของไบนารีที่ใหญ่ขึ้น (Code Bloat) และสร้างความเจ็บปวดให้กับฝั่งผู้เรียกใช้งาน เพราะเราไม่สามารถโยน Vec ที่สร้างจาก Allocator คนละตัวเข้าไปในฟังก์ชันเดียวกันได้โดยตรง

เมื่อเราหันไปมองภาษาอื่นอย่าง C++ ที่แก้ปัญหานี้ในมาตรฐาน C++17 ด้วยการใช้ Polymorphic Memory Resources (Type Erasure) หรือภาษา Zig ที่เลือกใช้โครงสร้างแบบ VTable (Dynamic Dispatch) เป็นค่าเริ่มต้น ซึ่งแม้ในทฤษฎีจะดูช้ากว่า แต่ในทางปฏิบัติกลับช่วยลดปัญหา Instruction Cache Misses จนทำงานได้เร็วกว่าในบางสภาวะ สำหรับตัว Rust เอง เราสามารถอาศัยท่า Box<dyn Allocator> เพื่อทำ Dynamic Dispatch ได้เช่นกัน และเราอาจคาดหวังความเก่งกาจของ LLVM ในการช่วยแกะรอยและแปลงกลับเป็น Static Inline ให้ในจังหวะคอมไพล์ ซึ่งจะช่วยให้เราได้ทั้งความยืดหยุ่นและประสิทธิภาพไปพร้อมๆ กัน

ก้าวต่อไปในปี 2026: 3 ทางเลือก

เมื่อมองภาพรวมทั้งหมดในปี 2026 เรากำลังยืนอยู่บนทางแยก 3 สายหลักครับ:

  1. สายแรก คือรอต่อไปจนกว่าจะมีทางเลือกใหม่ที่ดีกว่าแบบก้าวกระโดดโผล่ขึ้นมา
  2. สายที่สอง คือยอมหักดิบประกาศ Stabilize Trait ในสภาพปัจจุบันไปเลย แล้วยอมปล่อยมือจาก Use case เฉพาะทางอย่างงานเคอร์เนล
  3. สายที่สาม คือยอมประนีประนอม ปรับแต่ง Trait อีกเล็กน้อยโดยเพิ่มองค์ประกอบอย่าง Context, การบังคับ Layout แบบไม่เป็นศูนย์ และระบบ Error ที่ดีขึ้น เข้าไปเป็นส่วนหนึ่งของดีไซน์หลักก่อนจะประกาศใช้จริง

Credit & Reference:

  1. The State of Allocators in 2026