لماذا يستغرق تجميع C++ وقتا طويلا؟




performance compilation (10)

يستغرق تجميع ملف C ++ وقتاً طويلاً بالمقارنة مع C # و Java. يستغرق وقتًا طويلاً بشكل كبير في ترجمة ملف C ++ من تشغيل برنامج نصي Python حجم عادي. أنا الآن أستخدم VC ++ ولكنه نفس الشيء مع أي مترجم. لماذا هذا؟

كان السببان اللذان كان من الممكن أن أفكر بهما هو تحميل ملفات الرأس وتشغيل المعالج الأولي ، ولكن هذا لا يبدو أنه يجب أن يشرح لماذا يستغرق الأمر وقتًا طويلاً.


أكبر القضايا هي:

1) رأس لانهائي بالتصحيح. سبق ذكرها. عادة ما تعمل التخفيف (مثل # pragma مرة واحدة فقط) لكل وحدة تجميع ، وليس لكل بناء.

2) حقيقة أن toolchain غالبا ما يتم فصله إلى ثنائيات متعددة (make، preprocessor، compiler، assembler، archiver، impdef، linker و dlltool في الحالات القصوى) التي يجب على جميع إعادة تهيئة وإعادة تحميل كل الحالة طوال الوقت لكل استدعاء ( المترجم ، المجمع) أو كل اثنين من الملفات (أرشيفي ، رابط ، و dlltool).

راجع أيضًا هذه المناقشة على comp.compilers: http://compilers.iecc.com/comparch/article/03-11-078 خاصةً هذه:

http://compilers.iecc.com/comparch/article/02-07-128

لاحظ أن جون ، المشرف على comp.compilers يبدو أنه موافق ، وهذا يعني أنه من الممكن تحقيق سرعات مشابهة لـ C أيضًا ، إذا كان أحد يدمج أداة المفاتيح بالكامل ويقوم بتنفيذ الرؤوس التي تم ترجمتها مسبقًا. العديد من compilers C التجاري القيام بذلك إلى حد ما.

لاحظ أن نموذج Unix الخاص بتدوين كل شيء إلى ثنائي منفصل هو نوع من نموذج الحالة الأسوأ لنظام التشغيل Windows (مع إنشاء عملية بطيئة). من الملاحظ جداً عند مقارنة أوقات البناء في دول الخليج بين Windows و * nix ، خاصة إذا كان نظام التكوين / التكوين يستدعي بعض البرامج فقط للحصول على المعلومات.


التباطؤ ليس بالضرورة نفسه مع أي مترجم.

لم أستخدم دلفي أو كيليكس ولكن في أيام MS-DOS ، سوف يقوم برنامج Turbo Pascal بالتجميع الفوري تقريباً ، في حين أن برنامج Turbo C ++ المكافئ سيزحف فقط.

الاختلافان الرئيسيان كانا نظام وحدة قوية جدا وصياغة تسمح بالتجميع أحادي المرور.

من الممكن بالتأكيد أن سرعة التجميع لم تكن مجرد أولوية لمطوري برامج التجميع C ++ ، ولكن هناك أيضًا بعض المضاعفات المتأصلة في بنية C / C ++ التي تجعلها أكثر صعوبة في المعالجة. (أنا لست خبيراً في C ، ولكن Walter Bright ، وبعد بناء مختلف المجمعين التجاريين C / C ++ ، أنشأ لغة D. أحد التغييرات التي أجراها هو فرض قواعد خالية من السياق لجعل اللغة أسهل في التحليل .)

ستلاحظ أيضًا أنه يتم إعداد Makefiles بشكل عام بحيث يتم تصنيف كل ملف بشكل منفصل في C ، لذلك إذا كانت جميع ملفات المصدر 10 تستخدم نفس ملف التضمين ، فستتم معالجة الملف 10 مرات.


التحليل وتوليد الكود في الواقع سريع نوعا ما. المشكلة الحقيقية هي فتح وإغلاق الملفات. تذكر ، حتى مع ضم الحراس ، لا يزال المحول البرمجي مفتوح الملف .H ، وقراءة كل سطر (ثم تجاهل ذلك).

أخذ صديق مرة واحدة (في حين بالملل في العمل) ، تطبيق شركته ووضع كل شيء - كل الملفات المصدر والرأس - في ملف واحد كبير. انخفض وقت الترجمة من 3 ساعات إلى 7 دقائق.


بعض الأسباب هي:

1) قواعد C ++ أكثر تعقيدًا من C # أو Java وتستغرق وقتًا أطول في التحليل.

2) (أكثر أهمية) يقوم مترجم C ++ بإنتاج رمز الآلة ويقوم بجميع عمليات التحسين أثناء التحويل البرمجي. C # وجافا تذهب فقط في منتصف الطريق وترك هذه الخطوات إلى JIT.


سبب آخر هو استخدام المعالج المسبق C لتحديد موقع الإعلانات. حتى مع وجود حراس للرأس ، لا يزال يتعين إجراء تحليل مرارًا وتكرارًا ، في كل مرة يتم تضمينها فيها. يدعم بعض المترجمين الرؤوس المسبقة التي يمكن أن تساعد في ذلك ، ولكن لا يتم استخدامها دائمًا.

انظر أيضًا: C ++ الأسئلة الشائعة


ستحتاج اللغة المترجمة دائمًا إلى مقدار حمل أولي أكبر من اللغة المفسرة. بالإضافة إلى ذلك ، ربما لم تقم ببناء كود C ++ الخاص بك بشكل جيد. فمثلا:

#include "BigClass.h"

class SmallClass
{
   BigClass m_bigClass;
}

يجمع أبطأ بكثير من:

class BigClass;

class SmallClass
{
   BigClass* m_bigClass;
}

عدة اسباب:

  • ملفات الرأس: كل وحدة تجميع فردية تتطلب مئات أو حتى الآلاف من الرؤوس لتكون 1: محملة ، و 2: مترجمة. كل واحد منهم عادة ما يكون recompiled لكل وحدة التجميع ، لأن المعالج السابق ضمان أن نتيجة تجميع رأس قد تختلف بين كل وحدة التحويل البرمجي. (يمكن تعريف ماكرو في وحدة تجميع واحدة تقوم بتغيير محتوى العنوان).

    ربما يكون هذا هو السبب الرئيسي ، لأنه يتطلب تجميع كميات هائلة من الكود لكل وحدة تجميع ، وبالإضافة إلى ذلك ، يجب تجميع كل رأس عدة مرات (مرة واحدة لكل وحدة تجميع تتضمنه)

  • الربط: بمجرد تجميعها ، يجب ربط جميع ملفات الكائنات معًا. هذه في الأساس عملية متجانسة لا يمكن أن تكون متوازية بشكل جيد ، ويجب أن تعالج مشروعك بالكامل.

  • تحليل: بناء الجملة معقد للغاية للتحليل ، ويعتمد بشكل كبير على السياق ، ويصعب جدا لفك الغموض. هذا يأخذ الكثير من الوقت

  • القوالب: في C # ، تكون List<T> هي النوع الوحيد الذي يتم تجميعه ، بغض النظر عن عدد instantiations من القائمة التي لديك في البرنامج. في C ++ ، يمثل vector<int> نوعًا منفصلًا تمامًا عن vector<float> ، وسيتعين تصنيف كل منها على حدة.

    أضف إلى ذلك أن القوالب تكوّن "لغة فرعية" كاملة مكتملة يجب على المترجم أن يفسرها ، وهذا يمكن أن يصبح معقدًا بشكل يبعث على السخرية. يمكن حتى للشفرة البرمجية metaprogramming القالب البسيطة نسبيًا تحديد القوالب العودية التي تنشئ العشرات والعشرات من عمليات إنشاء النماذج. قد تؤدي النماذج أيضًا إلى أنواع معقدة للغاية ، مع أسماء طويلة بشكل يبعث على السخرية ، إضافة الكثير من العمل الإضافي إلى الرابط. (يجب أن تقارن الكثير من أسماء الرموز ، وإذا كانت هذه الأسماء يمكن أن تنمو إلى عدة آلاف من الأحرف ، يمكن أن تصبح مكلفة إلى حد ما).

    وبالطبع ، فإنها تؤدي إلى تفاقم مشكلات الملفات الرأسية ، نظرًا لأنه يجب تحديد القوالب بشكل عام في الرؤوس ، مما يعني أنه يجب تحليل المزيد من التعليمات البرمجية وتجميعها لكل وحدة تجميع. في رمز C عادي ، عادةً ما يحتوي الرأس على إعلانات أمامية فقط ، لكن القليل جدًا من الكود الفعلي. في C ++ ، فإنه ليس من غير المألوف بالنسبة لكافة التعليمات البرمجية الموجودة في ملفات رأس.

  • التحسين: يسمح C ++ ببعض التحسينات الدراماتيكية. لا تسمح C # أو Java بالفصل التام للصفوف (يجب أن تكون هناك لأهداف الانعكاس) ، ولكن حتى قالب بسيط من C ++ metaprogram يمكن أن يولد بسهولة العشرات أو المئات من الطبقات ، وكلها موضحة ومزالة مرة أخرى في التحسين مرحلة.

    علاوة على ذلك ، يجب تحسين برنامج C ++ بالكامل بواسطة المحول البرمجي. يمكن أن يعتمد برنامج AC # على المترجم JIT لإجراء تحسينات إضافية في وقت التحميل ، ولا تحصل C ++ على مثل هذه "الفرص الثانية". ما يولده المجمع هو الأمثل كما هو الحال.

  • رمز الماكينة: يتم تجميع C ++ إلى رمز الجهاز الذي قد يكون أكثر تعقيدًا من استخدام كود bytecode Java أو .NET (خاصة في حالة x86).
    (تم ذكر ذلك من حيث الاكتمال فقط لأنه ورد ذكره في التعليقات ، ومن الناحية العملية ، من غير المرجح أن تستغرق هذه الخطوة أكثر من جزء صغير من إجمالي وقت التجميع.)

تتم مشاركة معظم هذه العوامل بواسطة كود C ، والذي يتم تجميعه فعليًا بكفاءة عالية. تعتبر خطوة التحليل أكثر تعقيدًا في لغة C ++ ، ويمكن أن تستغرق وقتًا أطول بكثير ، ولكن ربما يكون الجاني الرئيسي هو القوالب. إنها مفيدة ، وتجعل لغة C ++ لغة أكثر قوة بكثير ، ولكنها أيضًا تؤثر سلبًا على سرعة التجميع.


كما علق بالفعل ، المجمع ينفق الكثير من الوقت instantiating و instantiating مرة أخرى على القوالب. إلى هذا المدى ، هناك مشاريع تركز على هذا البند بعينه ، وتطالب بتعجيل ملحوظ بمقدار 30 مرة في بعض الحالات الإيجابية. انظر http://www.zapcc.com .


يتم تجميع C ++ في رمز الجهاز. لذلك لديك المعالج المسبق ، المحول البرمجي ، المحسن ، وأخيرًا المجمع ، وكلها يجب أن تعمل.

يتم تصنيف Java و C # إلى رمز بايت / IL ، وتنفيذ Java virtual machine / .NET Framework (أو ترجمة JIT إلى رمز الجهاز) قبل التنفيذ.

بايثون هي لغة مفسرة يتم تجميعها أيضًا في كود بايت.

أنا متأكد من أن هناك أسبابًا أخرى لهذا أيضًا ، ولكن بصفة عامة ، فإن عدم الحاجة إلى الترجمة إلى لغة الآلة الأصلية يوفر الوقت.


بناء C / C ++: ما يحدث حقا ولماذا يستغرق وقتا طويلا

لا يتم إنفاق جزء كبير نسبيًا من وقت تطوير البرامج على الكتابة ، أو التشغيل ، أو تصحيح الأخطاء ، أو حتى تصميم الشفرة ، ولكن في انتظار الانتهاء من التجميع. من أجل جعل الأمور سريعة ، علينا أولاً أن نفهم ما يحدث عندما يتم تجميع برامج C / C ++. الخطوات هي كما يلي:

  • ترتيب
  • بناء أداة بدء التشغيل
  • فحص الاعتمادية
  • التحويل البرمجي
  • ربط

سننظر الآن في كل خطوة بمزيد من التفصيل مع التركيز على كيفية جعلها أسرع.

ترتيب

هذه هي الخطوة الأولى عند البدء في البناء. يعني عادة تشغيل برنامج نصي تكوين أو CMake أو Gyp أو SCons أو بعض الأدوات الأخرى. يمكن أن يستغرق ذلك أي شيء من دقيقة واحدة إلى عدة دقائق لكتابة نصوص تهيئة كبيرة قائمة على Autotools.

هذه الخطوة تحدث نسبيا نادرا. تحتاج فقط إلى تشغيل عند تغيير تكوينات أو تغيير تكوين الإنشاء. لا يوجد الكثير مما ينبغي القيام به لجعل هذه الخطوة بشكل أسرع ، وذلك بسبب عدم تغيير أنظمة الإنشاء.

بناء أداة بدء التشغيل

هذا ما يحدث عندما تقوم بإجراء أو انقر فوق رمز البناء على IDE (وهو عادةً اسم مستعار لإجراء). تبدأ الأداة الثنائية لأداة الإنشاء وتقرأ ملفات التكوين الخاصة بها بالإضافة إلى تكوين البنية ، والتي عادة ما تكون نفس الشيء.

استنادًا إلى درجة تعقيد البنية وحجمها ، يمكن أن يستغرق ذلك من أي جزء من ثانية إلى عدة ثوانٍ. في حد ذاته هذا لن يكون سيئا للغاية. ولسوء الحظ ، فإن معظم أنظمة البناء القائمة على الماكياج تجعل من الممكن استنساخ عشرات إلى مئات المرات لكل عملية بناء واحدة. عادة ما يحدث هذا عن طريق الاستخدام التعاودي للتكوين (وهو أمر سيء).

تجدر الإشارة إلى أن سبب جعل بطيء جدا ليس خطأ تنفيذ. يحتوي بناء الجملة من Makefiles بعض المراوغات التي تجعل تنفيذ سريع حقا ولكن مستحيلة. هذه المشكلة أكثر وضوحا عند دمجها مع الخطوة التالية.

فحص الاعتمادية

بمجرد قراءة أداة الإنشاء لتكوينها ، يجب عليها تحديد الملفات التي تم تغييرها وأيها تحتاج إلى إعادة ترجمة. تحتوي ملفات التكوين على رسم بياني دوري موصوف يصف اعتماديات البناء. عادةً ما يتم إنشاء هذا الرسم البياني أثناء خطوة التكوين. يتم تشغيل وقت بدء تشغيل أداة ومثبّت التبعية على كل بنية واحدة. يحدد وقت التشغيل المجمع الحد الأدنى على دورة التصحيح - ترجمة التحويل. للمشاريع الصغيرة هذه المرة عادة ما تكون بضع ثوان أو نحو ذلك. هذا مقبول. هناك بدائل لجعل. الأسرع منهم هو Ninja ، الذي تم إنشاؤه بواسطة مهندسي Google لـ Chromium. إذا كنت تستخدم CMake أو Gyp لبناء ، ما عليك سوى الانتقال إلى الخلفيات النينجا. ليس عليك تغيير أي شيء في ملفات الإنشاء نفسها ، فقط تمتع بزيادة السرعة. لا يتم حزم النينجا في معظم التوزيعات ، على الرغم من ذلك ، قد تحتاج إلى تثبيته بنفسك.

التحويل البرمجي

عند هذه النقطة نلجأ أخيرا إلى المجمع. قطع بعض الزوايا ، وهنا الخطوات التقريبية التي اتخذت.

  • يتضمن الدمج
  • تحليل الكود
  • توليد الكود / التحسين

خلافا للاعتقاد الشائع ، تجميع C ++ ليس في الواقع كل ذلك بطيء. STL بطيء ومعظم أدوات البناء المستخدمة لتجميع C ++ بطيئة. ومع ذلك ، هناك أدوات وطرق أسرع للتخفيف من الأجزاء البطيئة من اللغة.

استخدامهم يأخذ قليلا من الشحوم الكوع ، ولكن الفوائد لا يمكن إنكارها. تؤدي أوقات الإنشاء الأسرع إلى تطوير مطورين أكثر سعادة ورشاقة أكثر وأخيرًا رمز أفضل.





compilation