چگونه 10 میلیون خط کد C++ را به استاندارد C++14 (و سپس به C++17) ترجمه کردیم.

مدتی پیش (در پاییز 2016)، در طول توسعه نسخه بعدی پلت فرم فناوری 1C: Enterprise، این سوال در تیم توسعه در مورد پشتیبانی از استاندارد جدید مطرح شد. C ++ 14 در کد ما انتقال به یک استاندارد جدید، همانطور که فرض کردیم، به ما این امکان را می دهد که بسیاری از چیزها را زیباتر، ساده تر و قابل اعتمادتر بنویسیم و پشتیبانی و نگهداری کد را ساده تر می کند. و به نظر می‌رسد هیچ چیز خارق‌العاده‌ای در ترجمه وجود ندارد، اگر به خاطر مقیاس پایه کد و ویژگی‌های خاص کد ما نباشد.

برای کسانی که نمی‌دانند، 1C: Enterprise محیطی برای توسعه سریع برنامه‌های تجاری بین پلتفرمی و زمان اجرا برای اجرای آن‌ها در سیستم‌عامل‌ها و DBMS‌های مختلف است. به طور کلی، محصول شامل موارد زیر است:

  • خوشه سرور برنامه، روی ویندوز و لینوکس اجرا می شود
  • مشتری، کار با سرور از طریق http(s) یا پروتکل باینری خود، روی ویندوز، لینوکس، macOS کار می کند
  • مشتری وب، در حال اجرا در مرورگرهای کروم، اینترنت اکسپلورر، مایکروسافت اج، فایرفاکس، سافاری (نوشته شده در جاوا اسکریپت)
  • محیط توسعه (پیکربندی)، روی ویندوز، لینوکس، macOS کار می کند
  • ابزارهای مدیریت سرورهای برنامه، روی ویندوز، لینوکس، macOS اجرا می شوند
  • مشتری موبایل، اتصال به سرور از طریق http(s)، روی دستگاه های تلفن همراه دارای Android، iOS، Windows کار می کند
  • بستر تلفن همراه - چارچوبی برای ایجاد برنامه های موبایل آفلاین با قابلیت همگام سازی، در حال اجرا در اندروید، iOS، ویندوز
  • محیط توسعه 1C: ابزارهای توسعه سازمانی، نوشته شده در جاوا
  • سرور سیستم های تعاملی

ما سعی می کنیم تا حد امکان کدهای مشابهی را برای سیستم عامل های مختلف بنویسیم - پایه کد سرور 99٪ رایج است، پایه کد مشتری حدود 95٪ است. پلت فرم فناوری 1C: Enterprise در درجه اول به زبان C++ نوشته شده است و مشخصات کد تقریبی در زیر آورده شده است:

  • 10 میلیون خط کد ++C،
  • 14 هزار فایل
  • 60 هزار کلاس
  • نیم میلیون روش

و همه این مطالب باید به C++14 ترجمه می شد. امروز به شما خواهیم گفت که چگونه این کار را انجام دادیم و در این فرآیند با چه چیزی مواجه شدیم.

چگونه 10 میلیون خط کد C++ را به استاندارد C++14 (و سپس به C++17) ترجمه کردیم.

سلب مسئولیت

هر آنچه در زیر در مورد کار آهسته/سریع نوشته شده است، (نه) مصرف حافظه زیاد توسط پیاده سازی کلاس های استاندارد در کتابخانه های مختلف به معنای یک چیز است: این برای ما صادق است. کاملاً ممکن است که پیاده سازی های استاندارد برای وظایف شما مناسب ترین باشند. ما از وظایف خودمان شروع کردیم: داده هایی را که برای مشتریانمان معمولی بود، گرفتیم، سناریوهای معمولی را روی آنها اجرا کردیم، به عملکرد، میزان حافظه مصرفی و غیره نگاه کردیم و تجزیه و تحلیل کردیم که آیا ما و مشتریانمان از چنین نتایجی راضی هستیم یا خیر. . و بسته به آن عمل کردند.

آنچه ما داشتیم

در ابتدا، ما کد پلتفرم 1C:Enterprise 8 را در Microsoft Visual Studio نوشتیم. این پروژه در اوایل دهه 2000 شروع شد و ما یک نسخه فقط ویندوز داشتیم. به طور طبیعی، از آن زمان کد به طور فعال توسعه یافته است، بسیاری از مکانیسم ها به طور کامل بازنویسی شده اند. اما کد طبق استاندارد 1998 نوشته شده بود و مثلاً براکت‌های زاویه راست ما با فاصله از هم جدا شدند تا کامپایل‌سازی موفق شود، مانند این:

vector<vector<int> > IntV;

در سال 2006، با انتشار پلتفرم نسخه 8.1، ما شروع به پشتیبانی از لینوکس کردیم و به یک کتابخانه استاندارد شخص ثالث تغییر مکان دادیم. STLPort. یکی از دلایل انتقال کار با خطوط عریض بود. در کد ما از std::wstring استفاده می کنیم که بر اساس نوع wchar_t است. حجم آن در ویندوز 2 بایت و در لینوکس پیش فرض 4 بایت است. این منجر به ناسازگاری پروتکل‌های باینری ما بین کلاینت و سرور و همچنین داده‌های دائمی مختلف شد. با استفاده از گزینه های gcc، می توانید تعیین کنید که اندازه wchar_t در حین کامپایل نیز 2 بایت باشد، اما پس از آن می توانید استفاده از کتابخانه استاندارد کامپایلر را فراموش کنید، زیرا از glibc استفاده می کند که به نوبه خود برای wchar_t 4 بایتی کامپایل شده است. دلایل دیگر اجرای بهتر کلاس‌های استاندارد، پشتیبانی از جدول‌های هش و حتی شبیه‌سازی معنایی حرکت در داخل کانتینرها بود که ما فعالانه از آن‌ها استفاده کردیم. و یک دلیل دیگر، همانطور که می گویند آخرین اما نه کم اهمیت، اجرای زهی بود. ما کلاس خودمان را برای تار داشتیم، زیرا ... با توجه به ویژگی های نرم افزار ما، عملیات رشته به طور گسترده ای استفاده می شود و برای ما این مهم است.

رشته ما بر اساس ایده های بهینه سازی رشته است که در اوایل دهه 2000 بیان شد آندری الکساندرسکو. بعدها، زمانی که الکساندرسکو در فیس بوک کار کرد، به پیشنهاد او، خطی در موتور فیس بوک استفاده شد که بر اساس اصول مشابه کار می کرد (به کتابخانه مراجعه کنید. حماقت).

خط ما از دو فناوری بهینه سازی اصلی استفاده می کند:

  1. برای مقادیر کوتاه، یک بافر داخلی در خود شی رشته استفاده می‌شود (بدون نیاز به تخصیص حافظه اضافی).
  2. برای بقیه، مکانیک استفاده می شود کپی در نوشتن. مقدار رشته در یک مکان ذخیره می شود و در هنگام تخصیص/تغییر از شمارنده مرجع استفاده می شود.

برای سرعت بخشیدن به کامپایل پلتفرم، ما اجرای جریان را از نوع STLPort خود حذف کردیم (که از آن استفاده نکردیم)، این باعث شد تا ما حدود 20٪ کامپایل سریع‌تری داشته باشیم. متعاقباً مجبور به استفاده محدود شدیم بالا بردن. Boost از استریم استفاده زیادی می کند، به ویژه در API های سرویس خود (مثلاً برای ورود به سیستم)، بنابراین مجبور شدیم آن را تغییر دهیم تا استفاده از استریم حذف شود. این به نوبه خود مهاجرت به نسخه های جدید Boost را برای ما دشوار کرد.

راه سوم

هنگام انتقال به استاندارد C++14، گزینه های زیر را در نظر گرفتیم:

  1. STLPort را که اصلاح کردیم به استاندارد C++14 ارتقا دهید. این گزینه بسیار دشوار است، زیرا ... پشتیبانی از STLPort در سال 2010 متوقف شد و ما باید تمام کدهای آن را خودمان بسازیم.
  2. انتقال به یک اجرای STL دیگر سازگار با C++14. بسیار مطلوب است که این پیاده سازی برای ویندوز و لینوکس باشد.
  3. هنگام کامپایل برای هر سیستم عامل، از کتابخانه تعبیه شده در کامپایلر مربوطه استفاده کنید.

گزینه اول به دلیل کار زیاد کاملاً رد شد.

مدتی به گزینه دوم فکر کردیم. به عنوان نامزد در نظر گرفته می شود libc++، اما در آن زمان تحت ویندوز کار نمی کرد. برای پورت libc++ به ویندوز، باید کارهای زیادی انجام دهید - برای مثال، هر چیزی را که مربوط به thread ها، همگام سازی نخ ها و اتمیسیته است را خودتان بنویسید، زیرا libc++ در این قسمت ها استفاده می شود. API POSIX.

و ما راه سوم را انتخاب کردیم.

انتقال

بنابراین، ما مجبور شدیم استفاده از STLPort را با کتابخانه های کامپایلرهای مربوطه جایگزین کنیم (Visual Studio 2015 برای ویندوز، gcc 7 برای لینوکس، clang 8 برای macOS).

خوشبختانه، کد ما عمدتاً طبق دستورالعمل ها نوشته شده بود و از انواع ترفندهای هوشمندانه استفاده نمی کرد، بنابراین مهاجرت به کتابخانه های جدید با کمک اسکریپت هایی که جایگزین نام انواع، کلاس ها، فضاهای نام و شامل در منبع شدند، نسبتاً روان پیش رفت. فایل ها. مهاجرت بر 10 فایل منبع (از 000) تأثیر گذاشت. wchar_t با char14_t جایگزین شد. ما تصمیم گرفتیم استفاده از wchar_t را کنار بگذاریم، زیرا char000_t روی همه سیستم عامل ها 16 بایت می گیرد و سازگاری کد بین ویندوز و لینوکس را خراب نمی کند.

ماجراهای کوچکی وجود داشت. به عنوان مثال، در STLPort یک تکرار کننده می تواند به طور ضمنی به یک اشاره گر به یک عنصر فرستاده شود، و در برخی از نقاط کد ما از این استفاده شده است. در کتابخانه های جدید دیگر امکان انجام این کار وجود نداشت و این قسمت ها باید به صورت دستی تحلیل و بازنویسی می شدند.

بنابراین، انتقال کد کامل شده است، کد برای همه سیستم عامل ها کامپایل شده است. وقت تست هاست

آزمایش‌های پس از انتقال، افت عملکرد (در برخی مکان‌ها تا 20-30٪) و افزایش مصرف حافظه (تا 10-15٪) را نسبت به نسخه قدیمی کد نشان داد. این به ویژه به دلیل عملکرد پایین بهینه رشته های استاندارد بود. بنابراین، ما دوباره مجبور شدیم از خط خودمان، کمی تغییر یافته استفاده کنیم.

یکی از ویژگی‌های جالب اجرای کانتینرها در کتابخانه‌های تعبیه‌شده نیز آشکار شد: خالی (بدون عناصر) std::map و std::set از کتابخانه‌های داخلی حافظه اختصاص می‌دهند. و با توجه به ویژگی های پیاده سازی، در برخی از نقاط کد تعداد زیادی کانتینر خالی از این نوع ایجاد می شود. ظروف حافظه استاندارد کمی برای یک عنصر ریشه اختصاص داده شده است، اما برای ما این مهم بود - در تعدادی از سناریوها، عملکرد ما به طور قابل توجهی کاهش یافت و مصرف حافظه افزایش یافت (در مقایسه با STLPort). بنابراین در کد ما این دو نوع کانتینر از کتابخانه های داخلی را با پیاده سازی آنها از Boost جایگزین کردیم که این کانتینرها این ویژگی را نداشتند و با این کار مشکل کاهش سرعت و افزایش مصرف حافظه حل شد.

همانطور که اغلب پس از تغییرات در مقیاس بزرگ در پروژه های بزرگ اتفاق می افتد، اولین تکرار کد منبع بدون مشکل کار نمی کند، و در اینجا، به ویژه، پشتیبانی از اشکال زدایی تکرار کننده ها در اجرای ویندوز مفید بود. قدم به قدم جلو رفتیم و تا بهار 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 گیگابایت را اشغال کرد. این رفتار به دلیل ویژگی‌های کتابخانه‌های استاندارد مدرن است که نسبت به اندازه فایل‌های سرویس تولید شده کمتر انتقادی شده‌اند. این بر عملکرد برنامه کامپایل شده تأثیر نمی گذارد، اما باعث ایجاد مشکلاتی در توسعه می شود، به ویژه، زمان کامپایل را افزایش می دهد. الزامات فضای دیسک آزاد در سرورهای ساخت و ماشین‌های توسعه‌دهنده نیز در حال افزایش است. توسعه دهندگان ما روی چندین نسخه از پلتفرم به صورت موازی کار می کنند و صدها گیگابایت فایل میانی گاهی اوقات در کار آنها مشکل ایجاد می کند. مشکل ناخوشایند است، اما بحرانی نیست؛ ما حل آن را فعلا به تعویق انداخته ایم. ما فناوری را به عنوان یکی از گزینه های حل آن در نظر می گیریم ایجاد وحدت (به ویژه، گوگل از آن در هنگام توسعه مرورگر کروم استفاده می کند).

منبع: www.habr.com

اضافه کردن نظر