วิธีที่เราแปลโค้ด C++ 10 ล้านบรรทัดเป็นมาตรฐาน C++14 (และจากนั้นเป็น C++17)

เมื่อไม่นานมานี้ (ในฤดูใบไม้ร่วงปี 2016) ในระหว่างการพัฒนาเวอร์ชันถัดไปของแพลตฟอร์มเทคโนโลยี 1C:Enterprise คำถามเกิดขึ้นภายในทีมพัฒนาเกี่ยวกับการสนับสนุนมาตรฐานใหม่ C ++ 14 ในรหัสของเรา ตามที่เราสันนิษฐานไว้ การเปลี่ยนไปใช้มาตรฐานใหม่จะทำให้เราสามารถเขียนหลายๆ สิ่งได้อย่างสวยงาม เรียบง่าย และเชื่อถือได้มากขึ้น และจะทำให้การสนับสนุนและการบำรุงรักษาโค้ดง่ายขึ้น และดูเหมือนว่าจะไม่มีอะไรพิเศษในการแปล หากไม่ใช่เพราะขนาดของฐานโค้ดและคุณสมบัติเฉพาะของโค้ดของเรา

สำหรับผู้ที่ไม่ทราบ 1C:Enterprise เป็นสภาพแวดล้อมสำหรับการพัฒนาอย่างรวดเร็วของแอปพลิเคชันธุรกิจข้ามแพลตฟอร์มและรันไทม์สำหรับการดำเนินการบนระบบปฏิบัติการและ DBMS ที่แตกต่างกัน โดยทั่วไป ผลิตภัณฑ์ประกอบด้วย:

เราพยายามเขียนโค้ดเดียวกันสำหรับระบบปฏิบัติการที่แตกต่างกันให้มากที่สุด - ฐานรหัสเซิร์ฟเวอร์เป็นเรื่องธรรมดา 99% ฐานรหัสไคลเอนต์อยู่ที่ประมาณ 95% แพลตฟอร์มเทคโนโลยี 1C:Enterprise เขียนด้วยภาษา C++ เป็นหลัก และลักษณะโค้ดโดยประมาณได้รับด้านล่าง:

  • รหัส C++ 10 ล้านบรรทัด
  • 14 ไฟล์
  • 60 คลาส
  • ครึ่งล้านวิธี

และเนื้อหาทั้งหมดนี้ต้องแปลเป็น C++14 วันนี้เราจะมาเล่าให้คุณฟังว่าเราทำเช่นนี้ได้อย่างไรและพบอะไรบ้างในกระบวนการนี้

วิธีที่เราแปลโค้ด C++ 10 ล้านบรรทัดเป็นมาตรฐาน C++14 (และจากนั้นเป็น C++17)

ข้อจำกัดความรับผิดชอบ

ทุกสิ่งที่เขียนด้านล่างเกี่ยวกับการทำงานที่ช้า/เร็ว (ไม่ใช่) การใช้หน่วยความจำขนาดใหญ่โดยการใช้งานคลาสมาตรฐานในไลบรารีต่างๆ มีความหมายอย่างหนึ่ง: นี่เป็นเรื่องจริงสำหรับสหรัฐอเมริกา ค่อนข้างเป็นไปได้ที่การใช้งานแบบมาตรฐานจะเหมาะสมที่สุดสำหรับงานของคุณ เราเริ่มต้นจากงานของเราเอง: เรานำข้อมูลที่เป็นปกติสำหรับลูกค้าของเรา รันสถานการณ์ทั่วไปกับพวกเขา ดูที่ประสิทธิภาพ จำนวนหน่วยความจำที่ใช้ ฯลฯ และวิเคราะห์ว่าเราและลูกค้าของเราพอใจกับผลลัพธ์ดังกล่าวหรือไม่ . และพวกเขาก็ทำหน้าที่ขึ้นอยู่กับ

สิ่งที่เรามี

ในตอนแรก เราเขียนโค้ดสำหรับแพลตฟอร์ม 1C:Enterprise 8 โดยใช้ Microsoft Visual Studio โปรเจ็กต์นี้เริ่มต้นในต้นปี 2000 และเรามีเวอร์ชันสำหรับ Windows เท่านั้น โดยปกติแล้วตั้งแต่นั้นมาโค้ดก็ได้รับการพัฒนาอย่างแข็งขัน กลไกหลายอย่างได้ถูกเขียนใหม่ทั้งหมด แต่โค้ดนี้เขียนขึ้นตามมาตรฐานปี 1998 และตัวอย่างเช่น วงเล็บเหลี่ยมของเราถูกคั่นด้วยช่องว่างเพื่อให้การคอมไพล์สำเร็จ เช่นนี้

vector<vector<int> > IntV;

ในปี 2006 ด้วยการเปิดตัวแพลตฟอร์มเวอร์ชัน 8.1 เราได้เริ่มสนับสนุน Linux และเปลี่ยนไปใช้ไลบรารีมาตรฐานของบุคคลที่สาม STLPort. เหตุผลประการหนึ่งของการเปลี่ยนแปลงคือต้องทำงานแบบกว้างๆ ในโค้ดของเรา เราใช้ std::wstring ซึ่งยึดตามประเภท wchar_t ตลอด ขนาดใน Windows คือ 2 ไบต์ และใน Linux ค่าเริ่มต้นคือ 4 ไบต์ สิ่งนี้นำไปสู่ความไม่เข้ากันของโปรโตคอลไบนารีของเราระหว่างไคลเอนต์และเซิร์ฟเวอร์ รวมถึงข้อมูลถาวรต่างๆ เมื่อใช้ตัวเลือก gcc คุณสามารถระบุได้ว่าขนาดของ wchar_t ระหว่างการคอมไพล์คือ 2 ไบต์ด้วย แต่จากนั้น คุณก็สามารถลืมเกี่ยวกับการใช้ไลบรารีมาตรฐานจากคอมไพเลอร์ได้ เนื่องจาก มันใช้ glibc ซึ่งจะถูกคอมไพล์สำหรับ wchar_t ขนาด 4 ไบต์ เหตุผลอื่นๆ คือการนำคลาสมาตรฐานไปใช้ที่ดีขึ้น การรองรับตารางแฮช และแม้กระทั่งการจำลองความหมายของการย้ายภายในคอนเทนเนอร์ ซึ่งเราใช้อยู่ในปัจจุบัน และอีกเหตุผลหนึ่งอย่างที่พวกเขาพูดกันเป็นครั้งสุดท้ายแต่ไม่ท้ายสุดก็คือประสิทธิภาพของเครื่องสาย เรามีคลาสเครื่องสายเป็นของตัวเอง เพราะว่า... เนื่องจากลักษณะเฉพาะของซอฟต์แวร์ของเรา การดำเนินการกับสตริงจึงถูกนำมาใช้อย่างกว้างขวาง และสำหรับเรานี่เป็นสิ่งสำคัญ

สตริงของเรามีพื้นฐานมาจากแนวคิดการปรับสตริงให้เหมาะสมซึ่งแสดงออกมาในช่วงต้นทศวรรษ 2000 อังเดร อเล็กซานเดอร์สคู. ต่อมา เมื่อ Alexandrescu ทำงานที่ Facebook ตามคำแนะนำของเขา มีการใช้บรรทัดในกลไกของ Facebook ที่ทำงานบนหลักการที่คล้ายกัน (ดูห้องสมุด ความเขลา).

สายการผลิตของเราใช้เทคโนโลยีการปรับให้เหมาะสมหลักสองประการ:

  1. สำหรับค่าแบบสั้น จะใช้บัฟเฟอร์ภายในในอ็อบเจ็กต์สตริงเอง (ไม่ต้องการการจัดสรรหน่วยความจำเพิ่มเติม)
  2. สำหรับอย่างอื่นทั้งหมดจะใช้กลไก คัดลอกเมื่อเขียน. ค่าสตริงจะถูกเก็บไว้ในที่เดียว และใช้ตัวนับอ้างอิงระหว่างการกำหนด/การแก้ไข

เพื่อเร่งการคอมไพล์แพลตฟอร์ม เราได้แยกการใช้งานสตรีมออกจากตัวแปร STLPort ของเรา (ซึ่งเราไม่ได้ใช้) ซึ่งทำให้การคอมไพล์เร็วขึ้นประมาณ 20% ต่อมาเราจึงต้องใช้ประโยชน์อย่างจำกัด การส่งเสริม. Boost ใช้สตรีมอย่างหนัก โดยเฉพาะใน API ของบริการ (เช่น สำหรับการบันทึก) ดังนั้นเราจึงต้องแก้ไขเพื่อลบการใช้สตรีมออก ในทางกลับกัน ทำให้เราย้ายไปยัง Boost เวอร์ชันใหม่ได้ยาก

วิธีที่สาม

เมื่อเปลี่ยนไปใช้มาตรฐาน C++14 เราได้พิจารณาตัวเลือกต่อไปนี้:

  1. อัปเกรด STLPort ที่เราแก้ไขเป็นมาตรฐาน C++14 ทางเลือกนั้นยากมากเพราะว่า... การสนับสนุน STLPort ถูกยกเลิกในปี 2010 และเราจะต้องสร้างโค้ดทั้งหมดด้วยตัวเอง
  2. เปลี่ยนไปใช้การใช้งาน STL อื่นที่เข้ากันได้กับ C ++ 14 เป็นที่พึงปรารถนาอย่างยิ่งว่าการใช้งานนี้มีไว้สำหรับ Windows และ Linux
  3. เมื่อทำการคอมไพล์สำหรับแต่ละ OS ให้ใช้ไลบรารีที่สร้างไว้ในคอมไพเลอร์ที่เกี่ยวข้อง

ตัวเลือกแรกถูกปฏิเสธทันทีเนื่องจากมีงานมากเกินไป

เราคิดถึงตัวเลือกที่สองมาระยะหนึ่งแล้ว ถือเป็นผู้สมัคร libc++แต่ในขณะนั้นมันใช้งานไม่ได้กับ Windows หากต้องการพอร์ต libc++ ไปยัง Windows คุณจะต้องทำงานหลายอย่าง เช่น เขียนทุกอย่างเกี่ยวกับเธรด การซิงโครไนซ์เธรด และอะตอมมิกซิตีด้วยตัวเอง เนื่องจาก libc++ ใช้ในพื้นที่เหล่านี้ POSIX API.

และเราเลือกวิธีที่สาม

การเปลี่ยนแปลง

ดังนั้นเราจึงต้องแทนที่การใช้ STLPort ด้วยไลบรารีของคอมไพเลอร์ที่เกี่ยวข้อง (Visual Studio 2015 สำหรับ Windows, gcc 7 สำหรับ Linux, clang 8 สำหรับ macOS)

โชคดีที่โค้ดของเราเขียนตามหลักเกณฑ์เป็นหลักและไม่ได้ใช้กลอุบายที่ชาญฉลาดทุกประเภท ดังนั้นการโยกย้ายไปยังไลบรารีใหม่จึงดำเนินไปค่อนข้างราบรื่น ด้วยความช่วยเหลือของสคริปต์ที่แทนที่ชื่อประเภท คลาส เนมสเปซ และรวมไว้ในแหล่งที่มา ไฟล์. การย้ายข้อมูลส่งผลต่อไฟล์ต้นฉบับ 10 ไฟล์ (จาก 000 ไฟล์) wchar_t ถูกแทนที่ด้วย char14_t; เราตัดสินใจยกเลิกการใช้ wchar_t เพราะ char000_t ใช้เวลา 16 ไบต์ในทุกระบบปฏิบัติการ และไม่ทำลายความเข้ากันได้ของโค้ดระหว่าง Windows และ Linux

มีการผจญภัยเล็กๆ น้อยๆ ตัวอย่างเช่น ใน STLPort ตัววนซ้ำสามารถส่งไปยังตัวชี้ไปยังองค์ประกอบโดยปริยายได้ และในบางตำแหน่งในโค้ดของเรา ก็มีการใช้สิ่งนี้ ในห้องสมุดใหม่ ไม่สามารถทำได้อีกต่อไป และต้องวิเคราะห์และเขียนข้อความเหล่านี้ใหม่ด้วยตนเอง

ดังนั้นการย้ายโค้ดจึงเสร็จสมบูรณ์ โค้ดจะถูกคอมไพล์สำหรับระบบปฏิบัติการทั้งหมด ถึงเวลาสำหรับการทดสอบ

การทดสอบหลังการเปลี่ยนแปลงพบว่าประสิทธิภาพลดลง (ในบางสถานที่มากถึง 20-30%) และการใช้หน่วยความจำเพิ่มขึ้น (สูงสุด 10-15%) เมื่อเทียบกับโค้ดเวอร์ชันเก่า โดยเฉพาะอย่างยิ่งเนื่องมาจากประสิทธิภาพที่ต่ำกว่าประสิทธิภาพของสายอักขระมาตรฐาน ดังนั้นเราจึงต้องใช้เส้นของเราเองที่ปรับเปลี่ยนเล็กน้อยอีกครั้ง

คุณลักษณะที่น่าสนใจของการใช้งานคอนเทนเนอร์ในไลบรารีแบบฝังก็ถูกเปิดเผยเช่นกัน: ว่างเปล่า (ไม่มีองค์ประกอบ) std::map และ std::set จากไลบรารีในตัวจัดสรรหน่วยความจำ และเนื่องจากคุณสมบัติการใช้งานในบางสถานที่ในโค้ดจึงมีการสร้างคอนเทนเนอร์เปล่าประเภทนี้จำนวนมาก คอนเทนเนอร์หน่วยความจำมาตรฐานได้รับการจัดสรรเพียงเล็กน้อยสำหรับองค์ประกอบรูทหนึ่งองค์ประกอบ แต่สำหรับเราสิ่งนี้กลายเป็นเรื่องสำคัญ - ในหลาย ๆ สถานการณ์ ประสิทธิภาพของเราลดลงอย่างมากและการใช้หน่วยความจำเพิ่มขึ้น (เมื่อเทียบกับ STLPort) ดังนั้นในโค้ดของเรา เราได้แทนที่คอนเทนเนอร์ทั้งสองประเภทนี้จากไลบรารีในตัวด้วยการใช้งานจาก Boost โดยที่คอนเทนเนอร์เหล่านี้ไม่มีคุณสมบัตินี้ และวิธีนี้ช่วยแก้ปัญหาเรื่องการชะลอตัวและการใช้หน่วยความจำที่เพิ่มขึ้น

ตามที่มักเกิดขึ้นหลังจากการเปลี่ยนแปลงขนาดใหญ่ในโครงการขนาดใหญ่ การวนซ้ำครั้งแรกของซอร์สโค้ดไม่ได้ผลโดยไม่มีปัญหา และโดยเฉพาะอย่างยิ่งการสนับสนุนการดีบักตัววนซ้ำในการใช้งาน Windows นั้นมีประโยชน์ เราก้าวไปข้างหน้าทีละขั้นตอน และภายในฤดูใบไม้ผลิปี 2017 (เวอร์ชัน 8.3.11 1C:Enterprise) การโยกย้ายก็เสร็จสมบูรณ์

ผลของการ

การเปลี่ยนไปใช้มาตรฐาน C++14 ใช้เวลาประมาณ 6 เดือน โดยส่วนใหญ่แล้ว นักพัฒนาหนึ่งราย (แต่มีคุณสมบัติสูงมาก) ทำงานในโครงการนี้ และในขั้นตอนสุดท้ายของตัวแทนของทีมที่รับผิดชอบในพื้นที่เฉพาะที่เข้าร่วม เช่น UI คลัสเตอร์เซิร์ฟเวอร์ เครื่องมือการพัฒนาและการดูแลระบบ ฯลฯ

การเปลี่ยนแปลงนี้ทำให้งานของเราในการโยกย้ายไปใช้เวอร์ชันมาตรฐานล่าสุดง่ายขึ้นอย่างมาก ดังนั้นเวอร์ชัน 1C:Enterprise 8.3.14 (อยู่ระหว่างการพัฒนา ซึ่งมีกำหนดวางจำหน่ายในต้นปีหน้า) จึงถูกถ่ายโอนไปยังมาตรฐานแล้ว C++17.

หลังจากการโยกย้าย นักพัฒนามีตัวเลือกเพิ่มเติม หากก่อนหน้านี้เรามี STL เวอร์ชันที่แก้ไขแล้วและเนมสเปซ std หนึ่งรายการ ตอนนี้เรามีคลาสมาตรฐานจากไลบรารีคอมไพเลอร์ในตัวในเนมสเปซ std ในเนมสเปซ stdx - บรรทัดและคอนเทนเนอร์ของเราปรับให้เหมาะสมสำหรับงานของเรา ในบูสต์ - บูสต์เวอร์ชันล่าสุด และนักพัฒนาใช้คลาสเหล่านั้นที่เหมาะสมที่สุดเพื่อแก้ไขปัญหาของเขา

การใช้งานตัวสร้างการย้ายแบบ "ดั้งเดิม" ยังช่วยในการพัฒนา (ย้ายตัวสร้าง) สำหรับคลาสจำนวนหนึ่ง หากคลาสมีตัวสร้างการย้ายและวางคลาสนี้ไว้ในคอนเทนเนอร์ STL จะปรับการคัดลอกองค์ประกอบภายในคอนเทนเนอร์ให้เหมาะสม (เช่น เมื่อคอนเทนเนอร์ถูกขยายและจำเป็นต้องเปลี่ยนความจุและจัดสรรหน่วยความจำใหม่)

บินในครีม

บางทีผลลัพธ์ที่ไม่พึงประสงค์ที่สุด (แต่ไม่สำคัญ) ของการย้ายถิ่นก็คือ เรากำลังเผชิญกับปริมาณที่เพิ่มขึ้น ไฟล์ objและผลลัพธ์ทั้งหมดของบิลด์ที่มีไฟล์ระดับกลางทั้งหมดเริ่มใช้พื้นที่ 60–70 GB ลักษณะการทำงานนี้เกิดจากลักษณะเฉพาะของไลบรารีมาตรฐานสมัยใหม่ ซึ่งมีความสำคัญน้อยลงกับขนาดของไฟล์บริการที่สร้างขึ้น สิ่งนี้ไม่ส่งผลกระทบต่อการทำงานของแอปพลิเคชันที่คอมไพล์แล้ว แต่ทำให้เกิดความไม่สะดวกหลายประการในการพัฒนา โดยเฉพาะอย่างยิ่ง จะทำให้เวลาในการคอมไพล์เพิ่มขึ้น ข้อกำหนดสำหรับพื้นที่ว่างในดิสก์บนบิลด์เซิร์ฟเวอร์และบนเครื่องของนักพัฒนาก็เพิ่มขึ้นเช่นกัน นักพัฒนาของเราทำงานบนแพลตฟอร์มหลายเวอร์ชันพร้อมกัน และไฟล์ระดับกลางหลายร้อยกิกะไบต์บางครั้งก็สร้างปัญหาในการทำงาน ปัญหาไม่เป็นที่พอใจ แต่ไม่สำคัญ เราได้เลื่อนการแก้ปัญหาออกไปในตอนนี้ เรากำลังพิจารณาเทคโนโลยีเป็นหนึ่งในทางเลือกในการแก้ปัญหา สร้างความสามัคคี (โดยเฉพาะ Google จะใช้ในการพัฒนาเบราว์เซอร์ Chrome)

ที่มา: will.com

เพิ่มความคิดเห็น