1 คะแนน โดย GN⁺ 2025-07-10 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • หากคอมไพเลอร์ Rust ต้องการใช้ การรับประกันเรื่อง pointer aliasing ในการปรับแต่งประสิทธิภาพ จำเป็นต้องนิยามให้ชัดเจนว่าโค้ด unsafe ละเมิดกฎตรงไหน
  • Stacked Borrows เดิมได้เสนอเกณฑ์ดังกล่าวไว้ แต่ยังไม่รองรับแพตเทิร์นที่พบบ่อยในโค้ด unsafe Rust จริง และฟีเจอร์ล่าสุดของ borrow checker ได้เพียงพอ
  • Tree Borrows เปลี่ยนโครงสร้างหลักของ Stacked Borrows จากสแต็กเป็นทรี ทำให้สามารถแสดงแพตเทิร์นที่ถูกต้องได้มากขึ้น
  • จากการประเมิน Rust crate ที่ใช้งานแพร่หลายที่สุด 30,000 รายการ พบว่าปฏิเสธเทสต์เคสน้อยกว่า Stacked Borrows 54%
  • การพิสูจน์ด้วย Rocq ยืนยันว่า สามารถคงการปรับแต่งประสิทธิภาพเดิมส่วนใหญ่ไว้ได้ พร้อมเปิดทางให้การปรับแต่งใหม่อย่าง read-read reordering

กฎ aliasing ที่จำเป็นใน unsafe Rust

  • Rust ให้การรับประกันที่แข็งแกร่ง เช่น ความปลอดภัยของหน่วยความจำและการป้องกัน data race ด้วย ระบบชนิดข้อมูลแบบอิง ownership
  • อย่างไรก็ตาม ในพื้นที่ของ โค้ด unsafe ความปลอดภัยจะไม่ได้รับการรับประกันโดยอัตโนมัติ และจำเป็นต้องมีกฎแยกต่างหากที่โปรแกรมเมอร์ต้องปฏิบัติตาม
  • คอมไพเลอร์ต้องการใช้ประโยชน์จากการรับประกันของระบบชนิดข้อมูล โดยเฉพาะข้อมูลที่เกี่ยวกับ aliasing ของพอยน์เตอร์ เพื่อเพิ่มประสิทธิภาพการปรับแต่งภายในฟังก์ชัน
  • โค้ด unsafe ที่เขียนผิดอาจทำให้การปรับแต่งเหล่านี้ใช้ไม่ได้ ดังนั้นเกณฑ์ที่ชัดเจนว่าโค้ดแบบใดถือว่า “badly behaved” จึงสำคัญ
  • งานวิจัยเดิมอย่าง Stacked Borrows ได้นิยามเกณฑ์นี้ไว้ แต่ยังมีข้อจำกัด
    • ปฏิเสธแพตเทิร์นหลายแบบที่พบบ่อยในโค้ด unsafe Rust จริง
    • ไม่สะท้อนฟีเจอร์ขั้นสูงของ Rust borrow checker ที่เพิ่งถูกนำเข้ามา

แนวทางของ Tree Borrows และผลการประเมิน

  • Tree Borrows ถูกนิยามโดยแทนที่สแต็ก ซึ่งเป็นโครงสร้างหลักของ Stacked Borrows ด้วย ทรี
  • การเปลี่ยนโครงสร้างนี้ช่วยผ่อนคลายข้อจำกัดของโมเดลเดิม
    • จากการประเมิน Rust crate ที่ใช้งานแพร่หลายที่สุด 30,000 รายการ พบว่าจำนวนเทสต์เคสที่ถูกปฏิเสธลดลง 54% เมื่อเทียบกับ Stacked Borrows
  • การพิสูจน์ด้วย Rocq ยังยืนยันคุณสมบัติที่เกี่ยวข้องกับการปรับแต่งประสิทธิภาพ
    • คงการปรับแต่งส่วนใหญ่ที่ Stacked Borrows อนุญาตไว้ได้
    • รองรับการปรับแต่งใหม่ที่สำคัญอย่าง read-read reorderings ด้วย
  • Tree Borrows ได้รับ PLDI'25 Distinguished Paper Award
  • แหล่งข้อมูลที่เกี่ยวข้อง

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

 
GN⁺ 2025-07-10
ความคิดเห็นจาก Hacker News
  • บทความล่าสุดของ Ralf Jung ให้บริบทเพิ่มเติม: https://www.ralfj.de/blog/2025/07/07/tree-borrows-paper.html
    แถมยังมีการนำเสนอล่าสุดจากกลุ่มของ Ralf Jung ที่พยายามระบุสเปกอย่างแม่นยำของ semantics การทำงานของ Rust ในรูปแบบที่รันได้ โดยใช้ภาษาถิ่นของ Rust: https://youtube.com/watch?v=yoeuW_dSe0o
  • สงสัยว่าคำกล่าวที่ว่า “คอมไพเลอร์อยากใช้ประโยชน์จากการรับประกันของ type system เกี่ยวกับ aliasing ของพอยน์เตอร์ เพื่อเปิดทางให้ทำ optimization ภายในฟังก์ชันได้อย่างทรงพลัง” นั้นจริงแค่ไหน
    Torvalds โต้แย้งมานานแล้วว่ากฎ strict aliasing ของ C ให้โทษมากกว่าประโยชน์ และฟังดูน่าเชื่อ ตัวอย่างอยู่ที่นี่: https://lore.kernel.org/all/CAHk-=wgq1DvgNVoodk7JKc6BuU1m9Un... ถ้าสนใจหัวข้อนี้ เธรดเต็มก็ควรค่าแก่การอ่าน
    จากประสบการณ์ที่จำกัด ยังไม่เห็นว่า Rust จะแตกต่างโดยพื้นฐาน อย่างน้อยเมื่อมี unsafe เข้ามาเกี่ยวข้องก็ยิ่งเป็นแบบนั้น
    • เห็นด้วยว่า กฎ strict aliasing ของ C แย่มาก แต่กฎที่เสนอสำหรับ Rust นั้นต่างออกไปมาก
      มองว่ามันมีประโยชน์ต่อคอมไพเลอร์มากกว่า และเป็นภาระต่อโปรแกรมเมอร์น้อยกว่า อีกทั้งในภาษาก็มีทางออกจริง ๆ ด้วย: ใช้ raw pointer ได้ และยังมีเครื่องมือสำหรับตรวจโค้ดด้วย
      สุดท้ายแล้วก็เหมือนทุกอย่างในการออกแบบภาษา คือเป็นการประนีประนอม และใน Rust อาจเจอ sweet spot ใหม่สำหรับ optimization แบบนี้ก็ได้ ถูกหรือไม่ เวลาจะเป็นเครื่องพิสูจน์
    • กฎ aliasing ของ Rust ค่อนข้างต่างจาก C
      ใน C มีอุปกรณ์ระดับระเบิดนิวเคลียร์อย่าง restrict ซึ่งจากประสบการณ์ ใน clang และ gcc ดูเหมือนจะได้ผลก็ต่อเมื่อใส่ไว้กับพารามิเตอร์ของฟังก์ชันเท่านั้น การวิเคราะห์ aliasing ตามชนิดข้อมูลโดยทั่วไปใช้ยาก และคุณก็สร้างสำเนาของชนิด int64_t ได้ไม่จำกัดไม่ได้ และคงไม่อยากทำด้วย การที่ต้องบังคับใช้ memcpy เมื่อต้องการ reinterpret เป็นชนิดอื่นก็น่ารำคาญ
      ในทางกลับกัน reference ของ Rust ถูกกำหนดขอบเขตอย่างละเอียดตาม lifetime, scope และ mutability และไม่ได้สนใจ “ชนิดทางกายภาพ” เองมากนัก ดังนั้นจึงสามารถ reinterpret และสลับหน่วยความจำเดียวกันระหว่าง &mut i32/&i32 กับ &mut i64/&i64 ได้ ตราบใดที่ abstraction แบบ unsafe ไม่ให้ reference &mut ที่ซ้อนทับกันพร้อมกัน หรือแบ่ง &mut หนึ่งตัวออกเป็น &mut หลายตัวที่ไม่ซ้อนทับกัน ก็สามารถอ่านและเขียนค่าครึ่งหนึ่งหรือหลายค่าได้ด้วยการอ่าน/เขียน Rust แบบ safe ปกติ
    • สิ่งที่ Linus พูดเกี่ยวกับคอมไพเลอร์ควรฟังแบบเผื่อใจไว้บ้าง เขาเขียน เคอร์เนลระบบปฏิบัติการ ไม่ใช่คนเขียนคอมไพเลอร์ และทั้งสองเป็นคนละแขนงกันพอสมควร
      การวิเคราะห์ aliasing สำคัญมากต่อการได้ performance ที่ดีในปัจจุบัน อย่างไรก็ดี ควรจำไว้ว่าผลได้ที่มากที่สุดมาจาก heuristic ที่ง่ายที่สุด เช่น load สองครั้งที่ใช้ค่า SSA เดียวกันเป็นพอยน์เตอร์ ย่อม alias กันแน่นอน
      ในมุมของ LLVM นั้น BasicAA ทำหน้าที่แบบนั้น เป็นชุด heuristic ง่าย ๆ ที่ใกล้เคียงกับ “ถ้าตามจุดจัดสรรของ object ได้ ก็แก้ alias query ได้แบบแน่ชัด ไม่อย่างนั้นก็ไม่รู้”
      คำถามจริง ๆ คือคุณค่าของการวิเคราะห์ aliasing ที่เกินกว่าการตรวจพื้นฐานและชัดเจน เมื่อ alias query เข้าสู่ขั้นที่ไม่สามารถแก้ได้แบบง่าย ๆ แล้ว สิ่งที่ทำได้จากผลลัพธ์นั้นโดยมากก็ลดลงมาก และแทบเหลือแค่การหาความเสี่ยงในการย้ายโค้ด ประโยชน์จึงเล็กกว่ามาก
      หนึ่งในการทดลองที่อยากทำคือวัด speedup รวมที่การวิเคราะห์ aliasing ที่สมบูรณ์แบบตามทฤษฎีจะให้ได้ เดาว่าแม้แต่โค้ดที่ไม่ใช่ HPC อย่างเคอร์เนล Linux ก็น่าจะอยู่ราว 20%
      [1] ตรงนี้ไม่รวม optimization ระดับวีรบุรุษอย่างการแปลง layout ของข้อมูล ซึ่งจะไม่ลองทำหากไม่มีการวิเคราะห์ aliasing คุณภาพสูง ในทางปฏิบัติเรารู้อยู่แล้วว่าไม่มีการวิเคราะห์ aliasing แบบนั้น จึงคงไม่ลองทำ optimization เหล่านั้น และมองว่าไม่ควรนับรวมใน speedup ที่คาดไว้
    • strict aliasing ของ C กับ aliasing ของ Rust ต่างก็จัดการเรื่อง aliasing แต่เป็นคนละเรื่องกัน Rust ค่อนข้างชัดเจนว่าไม่ได้เลือกแนวทางของ C
      aliasing ของ C อิงตามชนิดข้อมูลเท่านั้น จึงมีอีกชื่อว่า type-based alias analysis หรือ TBAA
    • อยากเห็นการวิเคราะห์ที่ละเอียดกว่านี้ แต่กฎคร่าว ๆ ง่าย ๆ คือเอาส่วนทั้งหมดในคอมไพเลอร์ที่ส่งข้อมูล aliasing ไปยัง LLVM ออก แล้วดูว่า performance เป็นอย่างไร
      เจอข้ออ้างว่า noalias ช่วยเพิ่ม performance ตามเวลารันได้ประมาณ 5% แต่ชัดเจนว่าเอกสารนั้นเก่ามาก
      https://github.com/rust-lang/rust/issues/54878#issuecomment-...
  • Stacked Borrows ที่กล่าวถึงเคยมีเธรดในปี 2020 และ 2018 ด้วย
    https://news.ycombinator.com/item?id=22281205
    https://news.ycombinator.com/item?id=17715399
  • ดูการนำเสนอที่ PLDI ได้ด้วย: https://www.youtube.com/watch?v=CJi_Fcs4bak
  • ลองทดสอบข้ออ้างในตัวอย่างที่ 4 ของ paper ที่บอกว่าโค้ด Rust บางส่วนจะถูกปฏิเสธ แต่ในเวอร์ชันคอมไพเลอร์ stable ดูเหมือนจะไม่เป็นเช่นนั้น
    คำอธิบายดูเหมือนจะบอกว่า ถ้าสร้าง *mut i32 จาก &mut แล้วใช้ *x = 10 แทน write(x) ก็จะไม่ใช้ two-phase borrow แบบ implicit ดังนั้นคอมไพเลอร์ควรปฏิเสธ แต่จริง ๆ แล้วผ่าน
    • Stacked Borrows เป็นโมเดลรันไทม์ของ Miri ถ้ารันใน Miri เวอร์ชัน *x = 10; จะรายงานข้อผิดพลาด แต่เวอร์ชัน write(x); จะไม่เป็น
      ข้อผิดพลาดอยู่ในรูป “Undefined Behavior: attempting a write access using [...] but that tag does not exist in the borrow stack for this location”

ไม่มีเหตุผลที่ตัว rustc เองจะปฏิเสธทั้งสองกรณี เพราะ y เป็น *mut และจากมุมมองของระบบชนิดข้อมูลในเวลาคอมไพล์ มันไม่มีความสัมพันธ์ด้านการยืมหรืออายุการใช้งานกับ &mut ที่เป็น x

  • บทความอธิบายพฤติกรรมใน Tree Borrows model ที่เสนอขึ้น ไม่ใช่การทำงานของตัวตรวจสอบการยืมในปัจจุบัน
    ตัวตรวจสอบการยืมปัจจุบันใช้การวิเคราะห์ที่จำกัดกว่า จึงตรวจไม่พบความขัดแย้งเฉพาะกรณีนี้ระหว่างพอยน์เตอร์ดิบกับ mutable reference
  • เป็นผลงานที่ยอดเยี่ยม จำได้ว่าเมื่อหลายปีก่อนได้อ่าน ข้อกำหนด Tree Borrows บนเว็บไซต์ของ Nevin แล้วประทับใจมากกับวิธีที่แก้ปัญหาค่อนข้างยุ่งยากได้อย่างสง่างาม
    จากประสบการณ์จริง [1] [2] ก็อนุญาตให้ใช้โค้ดที่ใน Stacked Borrows ถือว่าผิดกฎ แต่สมเหตุสมผลได้
    [1] https://github.com/Voultapher/sort-research-rs/blob/main/wri... คอลัมน์ Miri
    [2] https://github.com/rust-lang/rust/blob/6b3ae3f6e45a33c2d95fa...
  • สำหรับผู้ที่สนใจ การใช้งานจริงใน Miri อยู่ที่นี่: https://github.com/rust-lang/miri/tree/master/src/borrow_tra...
  • สงสัยว่า Rust หรือภาษาโปรแกรมมิงในอนาคตจะพัฒนาไปสู่การอนุญาตให้มี ตัวตรวจสอบการยืมหลายแบบ ที่มีคุณสมบัติต่างกัน เช่น ความเร็วคอมไพล์ ความเร็วรันไทม์ และความยืดหยุ่นของอัลกอริทึม แล้วให้โปรเจกต์เลือกใช้ได้หรือไม่
    • Rust รองรับการสลับการใช้งานตัวตรวจสอบการยืมอยู่แล้ว
      เคยย้ายจากตัวตรวจสอบการยืมแบบอิงขอบเขตไปเป็นตัวตรวจสอบการยืมแบบ อายุการใช้งานที่ไม่เป็นไปตามศัพท์ (non-lexical lifetimes) และยังมีการใช้งาน Polonius รุ่นทดลองถัดไปเป็นตัวเลือกด้วย เพียงแต่เมื่อการใช้งานใหม่พร้อมสำหรับโปรดักชันแล้ว การใช้งานเก่าก็จะถูกทิ้งไป เพราะไม่มีเหตุผลให้เลือกใช้
      การตรวจสอบการยืมนั้นรวดเร็ว และการใช้งานใหม่ยอมรับโปรแกรมที่ถูกต้องได้มากขึ้นอย่างเคร่งครัด
      นอกจากนี้ยังมีชนิด Rc และ RefCell ที่ทำให้ได้ความยืดหยุ่นมากขึ้นโดยแลกกับต้นทุนการตรวจสอบตอนรันไทม์
    • มีหลายแนวทางอยู่แล้ว เช่น affine types ที่ Rust ใช้, linear types, effects, dependent types และ formal proofs
      ทั้งหมดมีต้นทุนและความสามารถต่างกันในด้านการนำไปใช้ ประสิทธิภาพ และประสบการณ์ของนักพัฒนา
      และสิ่งที่ภาษาส่วนใหญ่ที่ไม่ใช่ Rust มุ่งไปจริง ๆ คือผลิตภาพจากการจัดการทรัพยากรอัตโนมัติ กล่าวคือใช้การจัดการทรัพยากรอัตโนมัติไม่ว่าจะด้วยวิธีใด แล้วผสานหนึ่งในระบบชนิดข้างต้นเฉพาะกับเส้นทางที่สำคัญต่อประสิทธิภาพเท่านั้น
    • สิ่งที่ต้องการจริง ๆ น่าจะเป็น separation logic ที่อยู่เบื้องหลัง: ระบุ precondition ของฟังก์ชันอย่างแม่นยำ พิสูจน์เงื่อนไขระหว่างทางภายในฟังก์ชัน และให้ optimizer รับ “lemma” เหล่านั้นไป optimize ได้เต็มที่จนถึงขอบเขตที่ invariant ที่ระบุไว้ยอมให้ทำ
      ในบริบทนี้ “Rust” อาจมองได้ว่าเป็นเพียง “invariant ที่คนทั่วไปมักต้องการ” และ “ชุด optimization ที่สมมติ invariant ทั่วไปเหล่านั้น—ไม่มากไปกว่านั้นและไม่น้อยไปกว่านั้น”
    • ตัวตรวจสอบการยืมของ Rust มีต้นทุนเวลา compile ค่อนข้างน้อย และไม่มีผลต่อ การสร้างโค้ด (code generation) เลย
      เวลาคอมไพล์ส่วนใหญ่ถูกใช้ไปกับการตีความ trait, monomorphization, LLVM optimization passes และการลิงก์
    • เท่าที่เข้าใจ ตัวตรวจสอบการยืมมีแต่ false negative และไม่มี false positive ไม่ใช่หรือ?
      อาจเป็นคำถามโง่ ๆ ก็ได้ แต่สงสัยว่าทำไมไม่รันหลาย implementation บนเธรดคู่ขนาน แล้วให้ตัวที่ให้ผลบวกก่อนเป็นฝ่ายชนะ
  • ในบทความบอกว่า unsafe code สามารถทำให้มี mutable references หลายตัวไปยังตัวแปรเดียวกันอยู่พร้อมกันผ่านพอยน์เตอร์ได้ แบบนั้นไม่ใช่ undefined behavior หรือ?
    การใช้พอยน์เตอร์เพื่อทำให้มี mutable references หลายตัวไปยังตัวแปรเดียวกันในเวลาเดียวกันเป็น undefined behavior ถ้าไม่ได้เข้าใจเจตนาของบทความผิด ก็ดูเหมือนจะเป็นอย่างนั้น
    • แก่นของงานนี้คือการตรึง ขอบเขตที่แน่นอนของ undefined behavior ให้ชัดเจน
      โค้ดข้างต้นคอมไพเลอร์ Rust ยอมรับ แต่ละเมิดกฎ คำถามคือมันละเมิดกฎอะไร
      โดยเนื้อแท้ สิ่งที่ตัวตรวจสอบการยืมยอมรับคือสิ่งที่ถูกกฎหมาย ส่วน unsafe สามารถแสดงสิ่งที่ผิดกฎหมายหรือเป็น undefined behavior ได้ และยังมีชุดกฎที่กว้างกว่าสิ่งที่ตัวตรวจสอบการยืมตรวจได้ แต่ยังคงถูกกฎหมายและมีพฤติกรรมที่กำหนดชัดเจน
      เป้าหมายของงานวิจัยนี้คือการระบุชุดกฎนั้นอย่างแม่นยำ ภาพรวมใหญ่ ๆ ใกล้เคียงกับ “พอยน์เตอร์ที่เขียนได้ต้องไม่มี alias” แต่รายละเอียดอย่าง interior pointers, iterator invalidation และคำถามว่าปัญหาอยู่ที่การสร้างพอยน์เตอร์เสีย ๆ หรือการใช้งานพอยน์เตอร์นั้น เป็นเรื่องยากมาก
      บทความ Stacked Borrows ก่อนหน้านั้นเรียบง่ายกว่าแต่ก็จำกัดกว่า ทำให้ unsafe code ในโลกจริงมักไม่ผ่านกฎ Tree Borrows กว้างกว่าและอนุญาตโค้ดได้มากกว่า ขณะเดียวกันก็ยังพิสูจน์ได้ว่าปลอดภัย
    • ใช่ แต่ประเด็นคือมันละเมิดกฎข้อไหนอย่างแน่ชัด อะไรคือคำจำกัดความที่แม่นยำซึ่งบอกว่านั่นเป็น undefined behavior?
      Tree Borrows เสนอคำจำกัดความแบบนั้นพอดี
      ที่นี่คำว่า “โค้ดสามารถทำแบบนี้ได้” หมายถึง “สามารถเขียนโค้ดนี้ คอมไพล์ และรันได้ และถ้าไม่มีสิ่งอย่าง Tree Borrows ก็ไม่มีหลักฐานให้ยืนยันว่าโค้ดนี้มีปัญหา”
      ในเมื่อยอมรับแล้วว่าต้องบอกว่าโค้ดแบบนี้เป็น undefined behavior กล่าวคือจำเป็นต้องมีสิ่งอย่าง Tree Borrows ส่วนนี้ของบทความจึงเป็นการโต้แย้งว่าทำไมจึงต้องมีสิ่งนั้น
    • ดูเหมือนจะเข้าใจคำว่า “ทำได้” ผิด ใน unsafe code สามารถทำแบบนั้นได้จริง ๆ และก็ใช่ มันเป็น undefined behavior
      https://play.rust-lang.org/?version=stable&mode=debug&editio...
    • ถ้าดูจุดเริ่มต้นของย่อหน้าถัดไป เจตนาจะชัดที่สุด
      เนื้อหาคือเห็นได้ชัดว่านักพัฒนาคอมไพเลอร์ Rust ต้องการรองรับ aliasing optimization ดังนั้นจึงต้องมีวิธี “ตัด” ตัวอย่างโต้แย้งแบบข้างต้นออกจากขอบเขตที่พิจารณา
    • คิดว่านั่นแหละคือประเด็นหลัก มันง่ายเกินไปที่จะละเมิดข้อจำกัดอย่างการไม่อนุญาตให้มี mutable references หลายตัว

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

  • เพิ่งรู้ว่าหนึ่งในผู้เขียนคือ Neven Villani เป็นลูกชายของ Cédric Villani ผู้ได้รับเหรียญ Fields Medal ในปี 2010 เรียกได้ว่าลูกไม้หล่นไม่ไกลต้นจริง ๆ