LLVM: จุดที่มีปัญหา
(npopov.com)- วิเคราะห์อย่างรอบด้านถึง ข้อจำกัดเชิงโครงสร้างและหนี้ทางเทคนิคของโครงการ LLVM พร้อมชี้จุดที่ควรได้รับการปรับปรุงอย่างเป็นรูปธรรม
- นำเสนอคอขวดในการดำเนินโครงการโอเพนซอร์สขนาดใหญ่ เช่น การรีวิวไม่เพียงพอ, ความไม่เสถียรของ API, เวลา build และ compile, ความไม่เสถียรของ CI
- ปัญหาด้าน การออกแบบ IR ครอบคลุมการจัดการค่า undef, การเข้ารหัสเงื่อนไขจำกัด, ความหมายของ floating point และความไม่สมบูรณ์ของสเปก
- ชี้ปัญหาเชิงโครงสร้างระยะยาว เช่น ความต่างกันของ backend, ความสับสนในการจัดการ ABI, ความล่าช้าในการย้ายไปใช้ GlobalISel และ pass manager
- แทนที่จะมองสถานะของ LLVM ในแง่ลบ บทความนี้เสนอให้มองเป็น โอกาสสำหรับการปรับปรุงอย่างต่อเนื่องและการขยายการมีส่วนร่วม
ปัญหาเชิงโครงสร้างหลัก
-
การขาดแคลนศักยภาพด้านการรีวิว ถูกชี้ว่าเป็นคอขวดที่ใหญ่ที่สุด
- มีผู้เขียนโค้ดจำนวนมาก แต่มีผู้รีวิวน้อย ทำให้เกิดกรณีที่มีการ merge การเปลี่ยนแปลงที่ยังไม่ได้รับการตรวจสอบอย่างเพียงพอ
- ด้วยโครงสร้างที่ให้ผู้เขียนเป็นผู้รับผิดชอบในการขอรีวิว ผู้มีส่วนร่วมหน้าใหม่จึงหาผู้รีวิวที่เหมาะสมได้ยาก
- มีการกล่าวถึงการนำ ระบบมอบหมาย PR อัตโนมัติ ของ Rust มาใช้เป็นแนวทางปรับปรุง
-
การเปลี่ยนแปลง API และ IR บ่อยครั้ง (Churn) สร้างภาระให้ผู้ใช้
- C API ค่อนข้างเสถียร แต่ C++ API เปลี่ยนแปลงบ่อย ทำให้ต้นทุนในการดูแลรักษา frontend และ backend สูงขึ้น
- ด้วยแนวคิด “Upstream or GTFO” โค้ดที่ไม่ได้แชร์ขึ้น upstream จึงไม่ถูกนำมาพิจารณาในการตัดสินใจ
-
ปัญหา เวลา build ที่มากเกินไป
- LLVM ประกอบด้วยโค้ด C++ มากกว่า 2.5 ล้านบรรทัด ทำให้ใช้เวลา build นานมาก และในการ build แบบ debug การใช้หน่วยความจำและดิสก์จะเพิ่มสูงขึ้นอย่างมาก
- มีการพูดถึงแนวทางปรับปรุง เช่น precompiled headers (PCH), การ build แบบ dylib เป็นค่าเริ่มต้น, การทำ daemon ให้กับการทดสอบ
-
CI ไม่เสถียร
- แม้จะมี buildbot มากกว่า 200 ตัวทดสอบในสภาพแวดล้อมหลากหลาย แต่ก็ไม่สามารถรักษา “สถานะสีเขียว” ได้ตลอดเวลา
- ปัญหา flaky tests และปัญหาของ buildbot ทำให้สัญญาณเตือนถูกกลบ จนตรวจจับข้อผิดพลาดจริงได้ยาก
- การนำ pre-test สำหรับ PR เข้ามาช่วยปรับปรุงได้บางส่วน แต่ยังไม่แก้ปัญหาที่รากฐาน
-
การทดสอบแบบ end-to-end มีไม่เพียงพอ
- แม้ unit test ของการเพิ่มประสิทธิภาพแต่ละส่วนจะทำได้ดี แต่แทบไม่มีการทดสอบทั้ง pipeline หรือการทดสอบการเชื่อมต่อกับ backend แบบครบวงจร
- แม้จะมี
llvm-test-suiteอยู่แล้ว แต่ก็ยังครอบคลุมการผสมกันของการดำเนินการพื้นฐานและชนิดข้อมูลได้ไม่เพียงพอ
ปัญหาด้าน backend และประสิทธิภาพ
-
ความต่างกันระหว่าง backend
- แม้ส่วนกลางจะถูกรวมเป็นหนึ่งเดียว แต่ backend ของแต่ละ target ยังมีการแก้ไขแยกกันจำนวนมาก ทำให้ ความซ้ำซ้อนและการแตกแขนงเพิ่มขึ้น
- มีแนวโน้มจะเพิ่ม hook เฉพาะ target แทนการทำ optimization ส่วนกลางร่วมกัน
-
เวลา compile
- ช้าสำหรับ JIT และภาษาที่สร้าง IR ขนาดใหญ่ เช่น Rust และ C++
- การ build ด้วย
-O0ช้าเป็นพิเศษ และมีการเสนอว่า TPDE backend เป็นทางเลือกที่เร็วกว่าได้ถึง 10–20 เท่า
-
ขาดการติดตามประสิทธิภาพ
- ไม่มี โครงสร้างพื้นฐานอย่างเป็นทางการสำหรับติดตามประสิทธิภาพ runtime
- ระบบ LNT มีประสิทธิผลต่ำ เนื่องจากปัญหาความไม่เสถียรในการทำงาน, UX และข้อมูลที่ไม่เพียงพอ
ปัญหาการออกแบบ IR
-
ความซับซ้อนในการจัดการค่า undef
- เนื่องจากอาจมีค่าต่างกันได้ในแต่ละครั้งที่ถูกใช้งาน จึงก่อให้เกิดข้อผิดพลาดระหว่าง optimization
- ในอนาคตอาจถูกแทนที่ด้วย ค่า poison แต่การจัดการ poison ในหน่วยความจำยังไม่สมบูรณ์
-
สเปกไม่สมบูรณ์และไม่สอดคล้องกัน
- มีกรณีการทำงานผิดพลาดเก่าที่ยังไม่ได้รับการแก้ไขอยู่
- ยังมีโจทย์ด้านการออกแบบที่ยาก เช่น โมเดล provenance
- เพื่อแก้ปัญหานี้ จึงมีการตั้ง working group ด้านสเปกอย่างเป็นทางการ ขึ้นมา
-
ขาดความสม่ำเสมอในการเข้ารหัสเงื่อนไขจำกัด
- มีหลายวิธีปะปนกัน เช่น poison flags, metadata, attributes, assumes
- ส่งผลเสียต่อ optimization ทั้งจากการสูญเสียข้อมูลหรือการเก็บข้อมูลมากเกินไป
-
ปัญหาความหมายของ floating point (FP)
- เกิดความไม่สอดคล้องในกรณีของ signaling NaN, สภาพแวดล้อมที่ไม่เป็นมาตรฐาน, การจัดการ denormal, ความละเอียดเกินของ x87
- มีการแยกจัดการด้วย constrained FP intrinsic ทำให้ความซับซ้อนเพิ่มขึ้น
ปัญหาทางเทคนิคอื่น ๆ
-
ความล่าช้าในการย้ายระบบบางส่วน
- pass manager ใหม่ ถูกใช้ถึงแค่ขั้นกลางเท่านั้น ส่วน backend ยังใช้ของเก่าอยู่
- GlobalISel ผ่านมา 10 ปีแล้วยังไม่สามารถย้ายได้สมบูรณ์ และยังอยู่ร่วมกับ SDAG
-
ความสับสนในการจัดการ ABI และ calling convention
- การแยกความรับผิดชอบระหว่าง frontend และ backend ยังไม่ชัดเจน และเอกสารก็ไม่เพียงพอ
- กำลังมีการพัฒนา ไลบรารี ABI และ implementation ต้นแบบ
- ยังมีปัญหาที่ ABI เปลี่ยนไปตามการเปิดใช้ฟีเจอร์ของ target
-
ความไม่สอดคล้องในการจัดการ built-in functions และ libcalls
- TargetLibraryInfo และ RuntimeLibcalls แยกจากกัน ทำให้ขาดความสม่ำเสมอ
- ไม่สามารถรับรู้ความพร้อมใช้งานตามชนิดของ runtime library (เช่น libgcc, compiler-rt) ได้
- ขาดจุดสำหรับปรับแต่งโดย runtime ภายนอก เช่น Rust
-
โครงสร้าง Context / Module ที่ไม่มีประสิทธิภาพ
- type และ constant อยู่ใน Context ส่วน function และ global อยู่ใน Module
- ทำให้เข้าถึง data layout ไม่ได้ จึงไม่สะดวกต่อสิ่งอย่าง constant folding
- ไม่สามารถลิงก์ข้าม context ได้ และควรทำให้โครงสร้างเรียบง่ายกว่านี้
-
แรงกดดันต่อรีจิสเตอร์จาก LICM (loop-invariant code motion)
- มีการ hoist โดยไม่มี cost model
- backend ก็ไม่ sink กลับลงมาอีก ทำให้ จำนวน spill และ reload เพิ่มขึ้น
สรุป
- ปัญหาที่ยกมาทั้งหมดเป็น โจทย์เชิงโครงสร้างที่เกิดจากระดับความ成熟และขนาดของ LLVM และถูกนำเสนอในฐานะ โอกาสในการยกระดับคุณภาพของโครงการและประสบการณ์ของผู้มีส่วนร่วม
- ในบางด้าน เช่น การเพิ่มประสิทธิภาพการ build, ไลบรารี ABI, การติดตามประสิทธิภาพ มีการปรับปรุงที่กำลังดำเนินอยู่แล้ว
- โดยรวมแล้ว LLVM ยังทรงพลังมาก แต่ การรีแฟกเตอร์อย่างต่อเนื่องและการจัดระเบียบสเปกยังเป็นสิ่งจำเป็น
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
บทความทั้งชิ้นเรียบเรียงมาอย่างดี เลยรู้สึกเห็นด้วยมาก
ช่วงนี้ ความเสถียรของ LLVM IR สูงขึ้นมากพอสมควร ผมรีเบส Fil-C จาก LLVM 17 ไปเป็น 20 ได้ภายในวันเดียว
ในโปรเจ็กต์อื่นก็เคยคง pass เดียวกันไว้ข้าม LLVM หลายเวอร์ชัน และแทบไม่มีปัญหาใหญ่
แต่ แรงกดดันต่อรีจิสเตอร์ของ LICM รุนแรงมากเป็นพิเศษกับซอร์สที่ไม่ใช่ C/C++ ปัญหาดูเหมือนจะไม่ใช่ตัว LICM เองเท่าไร แต่เป็นเรื่องที่ต้องทำให้ regalloc เรียนรู้การ rematerialize ให้ดีขึ้น
ถ้าเปิดตัวเลือกให้มากขึ้นเพื่อให้ผู้พัฒนาฟรอนต์เอนด์ benchmark การตั้งค่าหลายแบบและเลือกค่าที่ดีที่สุดได้ ก็น่าจะดี
เครื่องมือและคอมโพเนนต์แต่ละตัวต่างก็มีกฎของตัวเอง การเกิดความต่างกันระหว่างเวอร์ชันเลยดูเป็นเรื่องธรรมชาติ เลยสงสัยว่าหรือผมจะเข้าใจผิด
ผมเคยขอให้ผู้ดูแล compiler-rt เปลี่ยน boolean แค่ตัวเดียวเพื่อจะ build LLVM 18 บน macOS แต่ issue กลับถูกล็อกเป็น “heated” และยังไม่ถูกแก้มา 4 ปีแล้ว
ถึงอย่างนั้นผมก็ยังรัก LLVM อยู่ดี clang-tidy, ASAN, UBSAN, LSAN, MSAN, TSAN ยอดเยี่ยมมาก
ผมคิดว่าการเขียนโค้ด C/C++ โดยไม่ใช้ clang-tidy เป็นการตัดสินใจที่ผิด
แต่ -fbounds-safety มีเฉพาะใน AppleClang ส่วน MSAN/LSAN มีเฉพาะใน LLVM Clang Xcode ก็ไม่มี clang-tidy, clang-format และ llvm-symbolizer ด้วย
สุดท้ายบน macOS เลยต้อง build Darwin LLVM มาใช้เอง
ฝั่ง Linux ก็ยังสับสน RHEL ไม่ให้ libcxx แต่ Fedora ให้ อย่างไรก็ตาม ไม่มีดิสโทรไหนมี libcxx ที่ instrumented สำหรับ MSAN
Fedora มาใกล้มากแล้วแต่ก็ยังต้อง build compiler-rt เองอยู่ดี
หลังจากตามการถกเถียงเกี่ยวกับ LLVM ช่วงหลัง ๆ ก็ยิ่งรู้สึกว่าจำเป็นต้องมี ชุดทดสอบแบบรันได้ที่เริ่มจาก LLVM IR โดยตรง ไม่ใช่จาก C
ถ้าลองทำแบ็กเอนด์เองจะพบว่าเอกสารเกี่ยวกับ SelectionDAG หรือ GlobalISel มีไม่พอ และความหมายของ operation ก็ไม่ชัด ทำให้ implement ผิดได้ง่าย
C API ให้ความรู้สึกเหมือนเป็นสิ่งที่ถูกละเลยใน LLVM หลายตัวเลือกและ opt pass ไม่ได้ถูกเปิดออกมา
นักพัฒนาส่วนใหญ่ใช้ C++ API โดยตรงกันอยู่แล้ว C API เลยถูกดันไปเป็นเรื่องรอง และสุดท้ายก็กลายเป็น พลเมืองชั้นสอง
การรีวิวโค้ดไม่ได้ให้รางวัลตอบแทนแบบทันที คนเลยไม่ค่อยอยากทำ
ถ้าให้เครดิตการมีส่วนร่วมกับการรีวิวด้วย ก็น่าจะช่วยสร้างแรงจูงใจได้
เมื่อ 6 ปีก่อน ผม build LLVM บ่อย ๆ บนโน้ตบุ๊ก Dell 9360 แรม 8GB ถ้าลด parallelism ตอนลิงก์ลงก็ยังทำได้ภายในข้อจำกัดด้านหน่วยความจำ
ตอนนี้ก็ยังสงสัยว่าแรม 8GB ยัง build ได้อยู่ไหม
ช่วงแรก ๆ ของ LLVM จุดเด่นคือ ความเร็วในการคอมไพล์ที่เร็วกกว่า GCC
ตอนนี้ผ่านมา 23 ปีของ LLVM แล้ว ก็สงสัยว่าจะมีอะไรใหม่โผล่มาอีกไหม
และก็ยังมีทางเลือกอย่าง Cranelift ที่ไม่ใช้ LLVM IR (Cranelift GitHub)
การจัดการ ABI และ calling convention คือความเจ็บปวดที่สุด
ในฟรอนต์เอนด์ของคอมไพเลอร์ต้องจัดการการส่งอาร์กิวเมนต์เอง และบางครั้งถึงขั้นต้อง คำนวณจำนวนรีจิสเตอร์ เอง
ในบทความบอกว่า “ฟรอนต์เอนด์ได้รับการปกป้องด้วย C API ที่เสถียร” แต่ในความเป็นจริงไม่ใช่แบบนั้น
บาง API เสถียร แต่ส่วนอย่าง Orc เปลี่ยนบ่อย
ดูเหมือน LLVM จะ แทบไม่มีระบบรีวิว issue เลย รายงานบั๊กที่ผมส่งไปก็ยังค้างอยู่หลายปี