سؤال

هل هناك عقوبة على أداء وقت التشغيل عند استخدام الواجهات (الفئات الأساسية المجردة) في C++؟

هل كانت مفيدة؟

المحلول

اجابة قصيرة:لا.

اجابة طويلة:ليست الفئة الأساسية أو عدد أسلاف الفئة في تسلسلها الهرمي هو ما يؤثر على سرعتها.الشيء الوحيد هو تكلفة استدعاء الأسلوب.

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

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

نصائح أخرى

الوظائف التي يتم استدعاؤها باستخدام الإرسال الظاهري غير مضمنة

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

تعتمد تكلفة المكالمة الافتراضية على النظام الأساسي

أما بالنسبة لغرامة المكالمة العامة مقارنة باستدعاء وظيفة عادية، فإن الإجابة تعتمد على النظام الأساسي المستهدف.إذا كنت تستهدف جهاز كمبيوتر مزودًا بوحدة المعالجة المركزية x86/x64، فإن عقوبة استدعاء وظيفة افتراضية تكون صغيرة جدًا، حيث يمكن لوحدة المعالجة المركزية x86/x64 الحديثة إجراء تنبؤ بالفرع على المكالمات غير المباشرة.ومع ذلك، إذا كنت تستهدف PowerPC أو أي منصة RISC أخرى، فقد تكون عقوبة المكالمة الافتراضية كبيرة جدًا، لأنه لا يتم التنبؤ بالمكالمات غير المباشرة على بعض الأنظمة الأساسية (راجع: أفضل ممارسات تطوير أجهزة الكمبيوتر الشخصية/Xbox 360 عبر الأنظمة الأساسية).

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

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

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

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

class AbstractAlgo
{
    virtual int func();
};

class Algo1 : public AbstractAlgo
{
    virtual int func();
};

class Algo2 : public AbstractAlgo
{
    virtual int func();
};

void compute(AbstractAlgo* algo)
{
      // Use algo many times, paying virtual function cost each time

}   

int main()
{
    int which;
     AbstractAlgo* algo;

    // read which from config file
    if (which == 1)
       algo = new Algo1();
    else
       algo = new Algo2();
    compute(algo);
}

الشيء نفسه باستخدام تعدد الأشكال وقت الترجمة

class Algo1
{
    int func();
};

class Algo2
{
    int func();
};


template<class ALGO>  void compute()
{
    ALGO algo;
      // Use algo many times.  No virtual function cost, and func() may be inlined.
}   

int main()
{
    int which;
    // read which from config file
    if (which == 1)
       compute<Algo1>();
    else
       compute<Algo2>();
}

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

يلاحظ معظم الناس عقوبة وقت التشغيل، وهم محقون في ذلك.

ومع ذلك، من خلال خبرتي في العمل على المشاريع الكبيرة، فإن الفوائد من الواجهات الواضحة والتغليف المناسب سرعان ما عوضت الزيادة في السرعة.يمكن تبديل التعليمات البرمجية المعيارية لتحسين التنفيذ، وبالتالي فإن النتيجة الصافية هي مكسب كبير.

قد يختلف عدد الأميال التي تقطعها، ومن الواضح أن ذلك يعتمد على التطبيق الذي تقوم بتطويره.

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

لاحظ أن الوراثة المتعددة تؤدي إلى تضخيم مثيل الكائن بمؤشرات vtable متعددة.مع G++ على x86، إذا كان فصلك يحتوي على طريقة افتراضية ولا توجد فئة أساسية، فلديك مؤشر واحد إلى vtable.إذا كان لديك فئة أساسية واحدة ذات أساليب افتراضية، فلا يزال لديك مؤشر واحد إلى vtable.إذا كان لديك فئتين أساسيتين باستخدام الأساليب الافتراضية، فلديك اثنين مؤشرات vtable على كل حالة.

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

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

الفرق الرئيسي الوحيد الذي أعرفه هو أنه نظرًا لأنك لا تستخدم فئة محددة، فإن عملية التضمين أصعب (كثيرًا؟)

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

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

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

نعم، ولكن لا شيء يذكر على حد علمي.يرجع سبب نجاح الأداء إلى "المراوغة" التي لديك في كل استدعاء للأسلوب.

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

إذا كنت تريد التأكد يجب عليك إجراء الاختبارات الخاصة بك.

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

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

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top