- ผสานตรรกะการเขียนโปรแกรมแบบ Datalog เข้ากับประสิทธิภาพของ Rust โดยเล่ากระบวนการพัฒนาเอนจิน Datalog แบบโต้ตอบได้ที่เรียบง่าย ใช้งานสะดวก และคำนึงถึงประสิทธิภาพอย่างละเอียด
- สามารถเพิ่ม/ขยายกฎ (rule) และข้อเท็จจริง (fact) ได้แบบเรียลไทม์ในสภาพแวดล้อม CLI ที่เข้าใจง่าย พร้อมรองรับการโหลดข้อมูลจำนวนมาก การป้อนกฎแบบไดนามิก และประสิทธิภาพการคิวรีที่รวดเร็ว
- อธิบาย Parsing, การแทนข้อมูล (Representation), และการประเมินกฎ (Planning/Evaluation) ทีละขั้นพร้อมโค้ด Rust เพื่อให้เรียนรู้วิธีลงมือสร้างจริงได้
- เริ่มจากการติดตั้งใช้งานแบบง่ายที่ยังไม่ปรับแต่ง แล้วค่อย ๆ ปรับปรุงทั้งประสิทธิภาพและโครงสร้าง ทำให้ได้เรียนรู้ตรรกะขั้นสูงอย่างการประมวลผลข้อมูลแบบขนานและการปรับแต่ง join
- ยกกรณีศึกษาการวิเคราะห์โปรแกรมบนชุดข้อมูลขนาดใหญ่ (เช่น Nullability, Aliasing) ให้รันจริง พร้อมแบ่งปันประสบการณ์ด้านประสิทธิภาพ ปัญหาหน่วยความจำ การปรับแต่งคิวรี และการปรับปรุง join-plan
บทนำ: ทดลองตรรกะการเขียนโปรแกรม Datalog ใน Rust
- ในช่วง Memorial Day ได้มีการฝึกปฏิบัติและอภิปรายเกี่ยวกับ logic programming หลากหลายแบบ (เช่น Datalog) ที่งานประชุม Minnowbrook
- เครื่องมือ Datalog เดิม ๆ (เช่น Soufflé, ctdal) พบข้อจำกัดในด้านการใช้งานจริง การขยายต่อ และประสิทธิภาพ จึงยิ่งเห็นความจำเป็นของเครื่องมือที่ใช้งานได้จริง
- ผู้เขียนจึงตัดสินใจลงมือสร้าง Datalog interpreter (
datatoad) ด้วย Rust เอง เพื่อให้ได้ทั้งความเรียบง่าย การใช้งานที่ดี และประสิทธิภาพ
- เป้าหมายของโปรเจกต์คือ โหลดข้อมูลขนาดใหญ่ใน CLI ได้รวดเร็ว เพิ่ม/แก้กฎแบบไดนามิก ดูผลลัพธ์แบบเรียลไทม์ และยังคงประสิทธิภาพการคิวรีไว้ได้
แนวคิดพื้นฐานของ Datalog
- Datalog อิงกับประโยคตรรกะในรูปกฎ (Head :- Body) และสามารถอนุมานข้อเท็จจริงที่สรุปได้ทั้งหมดจาก fact และ rule ที่กำหนดมาโดยอัตโนมัติ
- กฎ (เช่น
tri(a, b, c) :- edge(a, b), edge(b, c), edge(a, c)) ประกอบด้วยตัวแปรและลิเทอรัล
- fact คือค่าที่เป็นจริงโดยไม่มีเงื่อนไข (เช่น
edge(1, 2) :- .)
- จุดแข็งของ Datalog คือ เมื่อเพิ่มกฎ ชุดข้อมูลที่อนุมานได้จะเพิ่มขึ้นตามไปด้วย (monotonicity) และไม่ว่าลำดับของกฎ/ข้อเท็จจริงจะเป็นอย่างไร ก็ให้ผลลัพธ์เดียวกัน (convergence)
- ใน Rust มีการแทนกฎและ fact ด้วยโครงสร้าง Atom/Rule/Term และจัดการชุด fact แยกตาม relation
การออกแบบโครงสร้างหลัก
การแทนข้อมูล
- Fact ถูกออกแบบตั้งต้นเป็น
Vec<String> และชุด fact ใช้โครงสร้างอย่าง BTreeMap<String, Vec<Fact>>
- เพื่อปรับให้เหมาะกับข้อมูลขนาดใหญ่ จึงนำโครงสร้างข้อมูลแบบ columnar มาใช้เพื่อลด alloc overhead
- FactContainer: ชุด fact ที่จัดเรียงและลบข้อมูลซ้ำแล้ว ในโครงสร้างแบบ append-only
- FactLSM: วิธีจัดการ FactContainer หลายชั้นแบบ LSM (Log-structured merge-tree) เพื่อให้การแทรก การจัดเรียง และการผสานข้อมูลมีประสิทธิภาพขึ้น
- FactSet: จัดการวงจรชีวิตของ fact (เพิ่งเพิ่ม, เพิ่งอนุมานได้, และ fact ที่นิ่งแล้ว) เพื่อหลีกเลี่ยงการคำนวณซ้ำและการใช้หน่วยความจำเกินจำเป็น
การใช้กฎและการอนุมาน
- แต่ละกฎจะสร้าง JoinPlan ก่อน แล้วอนุมานด้วยวิธี merge join ตามลำดับคอลัมน์และชุดคีย์ที่เหมาะสม
- merge join: จัดเรียงคีย์คอลัมน์ของ body atom แต่ละตัว แล้วสร้าง fact ใหม่เฉพาะเมื่อ join key ตรงกัน เพื่อดึงประสิทธิภาพสูงสุด
- ใช้โครงสร้าง stable/recent/to_add ของ FactSet เพื่อแยก fact ที่อนุมานแล้วออกจาก fact ใหม่ และหลีกเลี่ยงการคำนวณซ้ำที่ไม่จำเป็น (differential evaluation)
- ลูป
.update(): ใช้ทุกกฎซ้ำไปเรื่อย ๆ จนไม่สามารถอนุมาน fact ใหม่ได้อีก และวนต่อจนถึงจุด fixpoint
การสร้าง parser
- รองรับไวยากรณ์สไตล์ Soufflé (
?var, :-, ., , ฯลฯ) และเขียน tokenizer/parser ขึ้นเองด้วย Rust
- เมื่อเกิดข้อผิดพลาดจะคืนค่า None อย่างปลอดภัย เป็น parser แบบเรียบง่ายที่เหมาะกับสภาพแวดล้อมเชิงทดลอง
การปรับประสิทธิภาพและตัวอย่างการวิเคราะห์จริง
การวิเคราะห์ Nullability (Reachability)
- นิยามกฎ Datalog และวัดประสิทธิภาพบนชุดข้อมูลขนาดใหญ่ (เช่น httpd_df) เพื่อติดตามเส้นทางการคัดลอก/ย้ายค่า
- รูปแบบการเขียนกฎที่ต่างกันทำให้ประสิทธิภาพต่างกันอย่างมาก (ขึ้นกับ variable binding, ลำดับคอลัมน์, การเกิด temp relation ตาม join plan ฯลฯ)
- รูปแบบข้อมูลตั้งต้นและกลยุทธ์ join ที่ต่างกัน ทำให้เวลาในการรันและการใช้หน่วยความจำต่างกันได้หลายสิบเท่า สะท้อนความสำคัญของการปรับคิวรีอย่างชัดเจน
- เมื่อปรับแต่งแล้ว พบว่าประสิทธิภาพดีกว่าเครื่องมือเดิมที่พัฒนาด้วย C++ (เช่น Graspan) ราว 10–80 เท่าหรือมากกว่า
การวิเคราะห์ Aliasing (Points-to)
- สร้างแพตเทิร์น Datalog ที่ซับซ้อนสำหรับการวิเคราะห์ aliasing/การติดตามพอยน์เตอร์ และรันคิวรีเดียวกับในงานวิจัย (Graspan, Zheng-Rugina ฯลฯ)
- ขยายโอเปอเรชันการทำซ้ำ (
^*), ออปชัน (^?), และ transpose (^T) ภายในกฎ Datalog ให้เป็น recursive/union แบบชัดเจน
- การออกแบบชื่อและการนำผลลัพธ์กลาง (เช่น relation alias, temp join) กลับมาใช้ใหม่ ส่งผลอย่างมากต่อประสิทธิภาพและการใช้ทรัพยากรของ query plan ทั้งหมด
- หากสร้างผลลัพธ์กลางขนาดใหญ่โดยไม่ปรับ query plan จะทำให้ประสิทธิภาพตกลงและหน่วยความจำพุ่งสูง (เช่น relation V)
- มีการอภิปรายถึงแนวทางแบบ "demand-driven" (magic-set transformation) ที่คำนวณเฉพาะผลลัพธ์ที่จำเป็น พร้อมชี้ให้เห็นความเป็นไปได้ในการดัดแปลง query plan และปรับปรุงประสิทธิภาพจริง
บทเรียนที่ได้จากการทดลองด้วย Rust โดยตรง
- หัวใจของประสิทธิภาพในเอนจิน Datalog อยู่ที่การแทนข้อมูลแบบ columnar/LSM การอนุมานเชิงส่วนต่าง และการปรับ query plan ของ join
- หากเขียนกฎแบบตรงไปตรงมาเชิงกลไกเพียงอย่างเดียว มักนำไปสู่การสร้างข้อมูลกลางที่ไม่จำเป็นและสิ้นเปลืองทรัพยากร จึงต้องมีการปรับแต่ง
- การทดลองด้วย Rust โดยตรงแสดงให้เห็นว่าสามารถจัดการ fact ระดับหลายล้านถึงหลายสิบล้านแถวบนชุดข้อมูลจริงได้อย่างมีประสิทธิภาพ พร้อมรักษาทั้งความสามารถในการขยายและความเร็วในการอนุมาน
- ในสภาพแวดล้อม CLI สามารถทดลองโหลดข้อมูลขนาดใหญ่ เพิ่มกฎแบบเรียลไทม์ และตรวจผลลัพธ์ได้อย่างสะดวก
- บทบาทของ query optimizer, แนวคิด bushy-tree join (ใช้ผลลัพธ์กลางให้เกิดประโยชน์), และการหลีกเลี่ยงการสร้าง relation ที่ไม่จำเป็น ล้วนเป็นข้อคิดสำคัญต่อการเขียนและใช้งาน Datalog จริง
งานต่อยอดในอนาคต
- ยังมีหัวข้อวิจัยต่อ เช่น disk spill, การกระจายงานด้วยหลาย worker/process, streaming join, และ custom compile optimization
- Rust Datalog มีศักยภาพสูงสำหรับงานจริง เช่น การวิเคราะห์โปรแกรมขนาดใหญ่ การอนุมานกราฟ/ความสัมพันธ์ การวิเคราะห์เชิงสถิต และการติดตาม data flow
1 ความคิดเห็น
ความคิดเห็นบน Hacker News