RAII, ภาพลวงตาของ Rust/Linux
(kristoff.it)สรุป
นี่เป็นบทความที่เขียนขึ้นจากการติดตาม ข้อขัดแย้งระหว่างนักพัฒนา Rust กับนักพัฒนา Linux เดิม แม้นักพัฒนาแต่ละคนจะมีสไตล์การเขียนโค้ดต่างกันได้ แต่โปรเจกต์ Linux ก็เคย ตัด C++ ออกไป เพื่อหลีกเลี่ยงสไตล์และโครงสร้างของมัน (RAII) มาแล้ว
วิธีการทำงานของโค้ดที่ Asahi Lina กล่าวถึงนั้น ช้าเกินไปเมื่อปิดโปรแกรมนั้น และขัดแย้งกับการประมวลผลแบบเป็นชุด ซึ่งเป็นวิธีพื้นฐานที่สุดสำหรับการสร้างซอฟต์แวร์ที่เน้นประสิทธิภาพ ตัวอย่างเช่น การใช้ memory arena เพื่อทำงานแบบเป็นชุดช่วยให้สามารถจัดการอายุการใช้งานหลายรายการให้เป็นหนึ่งเดียวได้ จึงไม่จำเป็นต้องใช้ RAII
นี่คือแหล่งข้อมูลที่ผมนำมาใช้สนับสนุนข้อโต้แย้งนี้ โดยทั้งหมดอธิบายว่าทำไมการประมวลผลแบบเป็นชุดจึงดี:
- Casey Muratori | Smart-Pointers, RAII, ZII? Becoming an N+2 programmer
- CppCon 2014: Mike Acton "Data-Oriented Design and C++"
- Modern Systems Programming: Rust and Zig - Aleksey Kladov
ดังนั้นผมจึงคิดว่า Linux ไม่ควรยอมรับ RAII ไปตลอดชีวิต
เหตุผลที่ผมนำบทความนี้มาคือ ผมเห็นนักพัฒนา Rust ชาวเกาหลีหลายคนโกรธกับบทความนั้นมากอยู่หลายครั้ง เลยอยากรู้ว่าทุกคนที่นี่คิดอย่างไรกันบ้าง คุณคิดเห็นอย่างไร?
11 ความคิดเห็น
นี่เป็นความเห็นส่วนตัวของผม แต่ก็พอเข้าใจความเป็นชนชั้นนำของนักพัฒนาบางคนอยู่บ้าง ในมุมมองเชิง "วิศวกรรม" ซอฟต์แวร์ โดยเฉพาะกับลินุกซ์แล้ว ทุกวันนี้หา "ซอฟต์แวร์" ที่ทั้งช่วยผลักดันปรัชญาโอเพนซอร์สและยังจับมือร่วมงานได้กว้างขวางแม้กับฝั่งปิดซอร์สได้ยากมาก ผมเลยสงสัยว่าพวกเขาอาจยิ่งยึดท่าทีอนุรักษ์นิยมที่ดู排外หรือถึงขั้นคล้ายพวกลัดไดต์ เพราะกลัวว่าโปรแกรมเมอร์ที่ยังไม่ผ่านการพิสูจน์ตัวเองจะหลั่งไหลเข้ามาพร้อมกับ Rust แล้วแปะโค้ดที่อยู่นอกการควบคุมของแกนหลักผู้ดูแลโครงการเดิมจนเต็มไปหมด เพิ่มหนี้ทางเทคนิคอย่างหนัก และทำให้อายุวงจรของลินุกซ์สั้นลงหรือเปล่า?
มันน่าสนใจดีที่เพื่อให้อโอเพนซอร์สยังคงเป็นโอเพนซอร์สได้ยาวนาน กลับต้องเลือกใช้ท่าทีที่ไม่ค่อย "เปิด" นัก
ผมเองก็ใช้และมักจะแนะนำ RAII หรือการจัดการทรัพยากรในลักษณะคล้ายกันอยู่บ่อย ๆ ครับ เพราะถึงจะไม่รู้ด้วยซ้ำว่า RAII คืออะไรแล้วใช้แบบไม่คิดมาก ก็ยังได้โค้ดที่ “อย่างน้อยก็ปลอดภัย” ออกมาอยู่ดี
แต่ถ้าใช้โดยไม่เข้าใจอย่างถูกต้อง ก็มีโอกาสสูงที่จะผลิตโค้ดที่ไม่มีประสิทธิภาพออกมามากมาย เช่น ทั้งที่เปิดไฟล์แค่ครั้งเดียวก็พอ กลับไปเปิดและปิดมันเป็นสิบ ๆ ครั้ง ผมคิดว่าถ้านักพัฒนายังคงใส่ใจเรื่องประสิทธิภาพอย่างสม่ำเสมอ และมีวัฒนธรรมแบบนั้นเป็นพื้นฐานในทีมพัฒนา ก็สามารถรีดประสิทธิภาพจาก RAII ได้ในระดับที่เพียงพอเหมือนกันครับ
freeทุกครั้งที่แต่ละออบเจ็กต์ถูกทำลายfreeไว้แล้วค่อยรันทีเดียวแบบ bulk?ใน Linux มีฟังก์ชันหรือ API อะไรที่ทำให้ข้อ 2 ทำงานได้เร็วกว่าข้อ 1 ไหม?
ผมคุ้นชินกับข้อ 1 มาตลอด เลยยังไม่ค่อยเข้าใจครับ
ไม่อยากย้อนกลับไปสู่ประสบการณ์การพัฒนาที่ต้องทำทุกอย่างเสร็จแล้วค่อยมานั่งหา memory leak ด้วย valgrind อีกแล้วครับ
ผมก็ไม่แน่ใจนัก แต่การบอกว่าจะไม่ใช้ RAII ฟังดูเหมือนว่าจะตั้งใจใช้ memory leak เพื่อดันประสิทธิภาพการ (ปิด) ขึ้นมา ไม่แน่ใจว่านี่เป็นแนวทางที่ถูกต้องหรือเปล่า
ยังไงก็ตาม ถ้าเป็นนักพัฒนาที่จัดการหน่วยความจำด้วยมือได้เก่งอยู่แล้ว ก็น่าจะใช้ RAII ได้ดีด้วย และถ้าเป็นนักพัฒนาที่พัฒนาโดยไม่มี RAII ไม่ได้ ก็คงจัดการหน่วยความจำด้วยมือไม่ได้เหมือนกัน เลยรู้สึกว่าไม่น่ามีเหตุผลที่จะไม่ใช้ RAII
ผมสงสัยว่า
freeกินเวลาไปมากแค่ไหน เลยลองเขียนโค้ดง่าย ๆ มาทดสอบดู แม้จะต่างจากเวิร์กโหลดจริงพอสมควรก็ตาม (ใช้ Rust release build และใช้std::alloc::allocกับstd::fs::File)ผมจัดสรรหน่วยความจำขนาดต่าง ๆ จำนวน 10,000,000 ก้อน รวมประมาณ 2.5GB แล้ววัดเฉพาะเวลาในการคืนหน่วยความจำ ปรากฏว่าใช้เวลา 1.87 วินาที หรือคิดเป็น 187ns ต่อก้อน
ในทางกลับกัน สำหรับไฟล์ ผมเปิดแฮนเดิลไว้ราว 10,000 ตัว แล้ววัดเฉพาะเวลาที่ใช้ปิด ปรากฏว่าใช้เวลาประมาณ 9 วินาที เท่ากับประมาณ 900us ต่อไฟล์หนึ่งไฟล์
(พีซี Windows เครื่องนี้น่าจะช้าเรื่องงานไฟล์เป็นพิเศษเพราะแอนติไวรัส บนโน้ตบุ๊ก Windows อีกเครื่องหนึ่งได้ประมาณ 400ns/200us และบนพีซี Linux อีกเครื่องหนึ่งได้ 50ns/600ns)
ในฐานะทางเลือกของ RAII มักมีการพูดถึงการจัดการแบบ bulk หรือการเชื่อให้ OS จัดการตอนโปรเซสจบแล้วปล่อยให้รีซอร์สรั่ว ซึ่งถ้าเป็นหน่วยความจำก็คงทำได้ไม่ยาก
แต่สำหรับรีซอร์สอย่างไฟล์หรือซ็อกเก็ต ผมไม่เคยเห็น API สำหรับเก็บกวาดแบบ bulk และถ้าปล่อยให้รีซอร์สรั่ว ถึงเวลาใน user code อาจลดลงก็จริง แต่เวลาที่ลดลงนั้นจะถูกย้ายไปเพิ่มเวลาให้เคอร์เนลใช้ในการปิดโปรเซสแทนแทบทั้งหมด จึงไม่ได้มีประโยชน์ด้านประสิทธิภาพมากนัก
RAII สำหรับหน่วยความจำเองก็ไม่ได้ช้าขนาดนั้นเมื่อเทียบกัน อีกทั้งไม่ได้เป็นเทคนิคที่ทำให้ใช้ arena ไม่ได้ และถ้าจำเป็นก็ไม่ได้ห้ามการทำให้รั่วโดยตั้งใจด้วย ดังนั้นจึงดูยากที่จะใช้เรื่องนี้เป็นเหตุผลในการหลีกเลี่ยง RAII
และในกรณีของ RAII สำหรับไฟล์ที่ช้ากว่า ซึ่งไม่มีทั้งวิธีจัดการแบบ bulk และไม่มีวิธีหลีกเลี่ยงต้นทุนนี้ ผมก็เลยสงสัยว่าทางเลือกอื่นของ RAII จะดีกว่าได้มากแค่ไหน
อาจจะนอกประเด็นไปเล็กน้อย แต่ผมรู้สึกว่าข้อโต้แย้งเกี่ยวกับ RAII และ lifetime มักถูกจำกัดวงอยู่แค่ทรัพยากรหน่วยความจำที่แทนด้วย malloc/free
RAII และ lifetime ไม่ได้มีประโยชน์แค่กับการจัดสรรหน่วยความจำเท่านั้น แต่ยังใช้ได้กว้างกับการโมเดลรีซอร์สส่วนใหญ่ที่มีการได้มาและคืนกลับ และระหว่างที่ครอบครองอยู่ต้องมีการควบคุมการเข้าถึงแบบเอกสิทธิ์ เช่น ไฟล์ ซ็อกเก็ต ล็อก ซึ่งเป็นทรัพยากรของ OS รวมถึง object pool, connection pool เป็นต้น
รีซอร์สเหล่านี้ก็มีโครงสร้างร่วมกับ malloc/free จึงมีปัญหาเชิงโครงสร้างแบบเดียวกัน เช่น การรั่ว use after free และ double free และเพราะมีโครงสร้างแบบเดียวกันนี่เอง จึงควรมีการให้ความสำคัญมากขึ้นกับข้อเท็จจริงที่ว่า RAII และ lifetime ไม่ได้แก้ปัญหาแค่เรื่องหน่วยความจำ แต่แก้ปัญหาของรีซอร์สประเภทนี้ไปพร้อมกันได้ด้วย
ตัวอย่างเช่น ใน Rust แม้แต่กับ file handle ก็สามารถป้องกัน use after close และ double close ได้ตั้งแต่คอมไพล์ไทม์: https://play.rust-lang.org/?version=stable&mode=debug&edition=…
ภาษา GC หลัก ๆ แม้จะจัดการหน่วยความจำด้วย GC แต่สุดท้ายก็ยังต้องเพิ่มกลไกพิเศษสำหรับรีซอร์สที่ต้องการการจัดการแบบกำหนดเวลาแน่นอน เช่น file handle หรือ socket ไม่ว่าจะเป็นโครงสร้างแบบ RAII เอง (เช่น
try-with-resourcesของ Java,usingของ C#,withของ Python) หรือโครงสร้างที่คล้ายกัน (เช่นdeferของ Go)ผลก็คือภาษาหนึ่งภาษาต้องมีโหมดการจัดการรีซอร์สหลายแบบอยู่ดี ซึ่งผมรู้สึกว่าสู้ใช้แนวทางนี้ไปเลยน่าจะดีกว่า
หากสิ่งที่คุณหมายถึงคือ arena ใน Rust ก็มี arena อยู่แล้วเช่นกัน และยังสามารถใช้ lifetime เพื่อทำให้อารีนาหมดไป แล้วห้ามเข้าถึงองค์ประกอบของอารีนาหลังจาก "ปลดปล่อยแบบยกชุด" ได้ด้วย โปรดดู https://crates.io/keywords/arena
ผมหวังว่าจะมีภาษาใหม่ ๆ ออกมาอีกมากแม้หลังจาก zig หรือ rust แต่จนถึงตอนนี้ก็ยังไม่เคยเห็นภาษาที่เหมาะสมได้เท่า rust เลยครับ กลับกันผมคิดว่าความรู้ระหว่างนักพัฒนาที่ผุดขึ้นมาจากการถกเถียงกันระหว่างภาษาเหล่านี้ต่างหากที่มีประโยชน์ ฮ่าฮ่า..
ผมเองก็เป็นนักพัฒนาที่ใช้ Rust เป็นภาษาหลักพอตัวเหมือนกัน ไม่ได้ถึงกับโกรธ แต่ก็รู้สึกว่าเขายกตัวอย่างที่ค่อนข้างสุดโต่งเกินไปหรือเปล่า (เช่นประเด็นว่า "ตอนปิดโปรแกรมนั้นช้าเกินไป" และแม้แต่ในวิดีโอที่ลิงก์ไว้ก็ยังยกกรณีที่ไม่ได้เกี่ยวข้องโดยตรงกับกรณีในโปรเจ็กต์ Rust เลย คือกรณีที่ตอนปิด Visual Studio ต้องเรียก destructor ของคอมโพเนนต์แต่ละตัว ทำให้ใช้เวลานานมาก)
ในกรณีที่เพื่อประสิทธิภาพจำเป็นต้องจัดการ Clean-up ของหลายคอมโพเนนต์พร้อมกันทีเดียว ก็ดูเหมือนว่าสามารถเลือกแนวทางไม่ implement
Dropให้กับคอมโพเนนต์แต่ละตัว แต่ไป implementDropให้กับ type ที่ถืออายุการใช้งานของคอมโพเนนต์เหล่านั้น เพื่อให้ทำ Clean-up รวดเดียวได้ น่าจะเป็นวิธีที่ใช้ได้ครับ ถ้ามีการใส่มาตรการป้องกันให้สร้างคอมโพเนนต์นั้นได้ผ่าน API ของ type นั้นเท่านั้นด้วยก็ยิ่งดีแน่นอนว่าความกังวลของผู้เขียนบทความข้างต้นน่าจะเป็นว่า หากแนวปฏิบัติการใช้ RAII เข้าไปอยู่ในโค้ดเบสของ Linux ก็อาจมีโค้ดที่มีความกังวลด้านประสิทธิภาพแบบแฝงอย่างมากสะสมเพิ่มขึ้นเรื่อย ๆ ท่ามกลางความซับซ้อนของโค้ดเบสขนาดมหาศาล และในระยะยาวอาจเกิดเรื่องคล้ายกับ Visual Studio ได้ ซึ่งผมคิดว่าเป็นจุดที่น่ากังวลอย่างเพียงพอครับ แต่ในอีกด้านหนึ่ง อย่างที่มีคอมเมนต์อื่นพูดไว้ RAII ก็ให้ความปลอดภัยที่ชัดเจนอยู่เหมือนกัน ดังนั้นผมมองว่าการเลือกใช้คงเป็นเรื่องของ trade-off พอสมควรครับ.
ทั้งสองฝ่ายต่างก็พูดถูกทั้งคู่
ถ้าจะยกอุปมา ในเกมออนไลน์ LoL มีภาพจำกันว่าตัวละคร Azir เป็นแชมเปียนระดับท็อปเทียร์ที่ยอดเยี่ยมอย่างท่วมท้นทั้งในด้านการเล่นแยกดัน การคุมพื้นที่ในทีมไฟต์ และมูลค่าของอัลติเมต แต่เรื่องนี้ใช้ได้ก็เฉพาะในการแข่งขันระดับโปรที่ต้องอาศัยความชำนาญสูงมากเท่านั้น สำหรับผู้เล่นทั่วไปแล้ว ช่วงเลนก็อ่อนเกินไป พื้นฐานตัวละครก็อ่อน จนเป็นได้แค่แชมเปียนระดับล่างสุดเท่านั้น
จากมุมมองของคนอย่าง Asahi Lina ที่มีความรู้ด้านการเขียนโปรแกรมและระบบปฏิบัติการสูงกว่าระดับบน 10% ขึ้นไป ทางเลือกอื่นนอกเหนือจาก RAII ก็คงดีกว่าอย่างเห็นได้ชัด แต่ในส่วนที่คนอีก 90% ต้องรับมือ ผมคิดว่าไม่มีอะไรดีไปกว่า RAII หรือ Rust แล้ว
อย่างไรก็ตาม หนึ่งในเหตุผลสำคัญที่ต้องรับประกัน memory stability/safety ก็คือปัญหาด้านความปลอดภัย... ดังนั้นผมมองว่า tradeoff เป็นสิ่งที่หลีกเลี่ยงไม่ได้
ถ้าไม่มี RAII นักพัฒนาที่มีประสบการณ์ค่อนข้างน้อยก็น่าจะสร้างบั๊กออกมาเป็นจำนวนมาก
อย่างน้อยในระดับแอปพลิเคชันที่ไม่ใช่ OS ก็...