ما هي تكلفة استخدام مؤشر لوظيفة العضو مقابل.محول مفتاح؟

StackOverflow https://stackoverflow.com/questions/113150

  •  02-07-2019
  •  | 
  •  

سؤال

لدي الحالة التالية:


class A
{
public:
    A(int whichFoo);
    int foo1();
    int foo2();
    int foo3();
    int callFoo(); // cals one of the foo's depending on the value of whichFoo
};

في تطبيقي الحالي أقوم بحفظ قيمة whichFoo في عضو بيانات في المُنشئ واستخدم a switch في callFoo() لتحديد أي من الأشخاص يجب الاتصال به.وبدلاً من ذلك، يمكنني استخدام أ switch في المنشئ لحفظ المؤشر إلى اليمين fooN() ليتم استدعاؤهم callFoo().

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

إيضاح:أدرك أنك لا تعرف أبدًا النهج الذي يوفر أداءً أفضل حتى تجربه وتوقيته.ومع ذلك، في هذه الحالة، قمت بالفعل بتنفيذ النهج 1، وأردت معرفة ما إذا كان النهج 2 يمكن أن يكون أكثر كفاءة من حيث المبدأ على الأقل.يبدو أنه يمكن أن يكون كذلك، والآن من المنطقي بالنسبة لي أن أكلف نفسي عناء تنفيذه وتجربته.

أوه، وأنا أيضًا أحب النهج 2 بشكل أفضل لأسباب جمالية.أعتقد أنني أبحث عن مبرر لتنفيذه.:)

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

المحلول

ما مدى تأكدك من أن استدعاء وظيفة العضو عبر المؤشر يكون أبطأ من مجرد الاتصال بها مباشرة؟هل يمكنك قياس الفرق؟

بشكل عام، يجب ألا تعتمد على حدسك عند إجراء تقييمات الأداء.اجلس مع المترجم الخاص بك ووظيفة التوقيت، وفي الواقع يقيس الخيارات المختلفة.قد تتفاجأ!

مزيد من المعلومات:هناك مقالة ممتازة مؤشرات وظيفة الأعضاء وأسرع مندوبين ممكنين لـ C++ والذي يخوض في تفاصيل عميقة جدًا حول تنفيذ مؤشرات وظيفة العضو.

نصائح أخرى

يمكنك كتابة هذا:

class Foo {
public:
  Foo() {
    calls[0] = &Foo::call0;
    calls[1] = &Foo::call1;
    calls[2] = &Foo::call2;
    calls[3] = &Foo::call3;
  }
  void call(int number, int arg) {
    assert(number < 4);
    (this->*(calls[number]))(arg);
  }
  void call0(int arg) {
    cout<<"call0("<<arg<<")\n";
  }
  void call1(int arg) {
    cout<<"call1("<<arg<<")\n";
  }
  void call2(int arg) {
    cout<<"call2("<<arg<<")\n";
  }
  void call3(int arg) {
    cout<<"call3("<<arg<<")\n";
  }
private:
  FooCall calls[4];
};

حساب مؤشر الوظيفة الفعلي خطي وسريع:

  (this->*(calls[number]))(arg);
004142E7  mov         esi,esp 
004142E9  mov         eax,dword ptr [arg] 
004142EC  push        eax  
004142ED  mov         edx,dword ptr [number] 
004142F0  mov         eax,dword ptr [this] 
004142F3  mov         ecx,dword ptr [this] 
004142F6  mov         edx,dword ptr [eax+edx*4] 
004142F9  call        edx 

لاحظ أنه ليس عليك حتى إصلاح رقم الوظيفة الفعلي في المُنشئ.

لقد قارنت هذا الرمز بـ asm الذي تم إنشاؤه بواسطة a switch.ال switch الإصدار لا يوفر أي زيادة في الأداء.

للإجابة على السؤال المطروح:على مستوى التفاصيل الدقيقة، سيعمل المؤشر إلى وظيفة العضو بشكل أفضل.

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

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

ملحوظة:لم أعمل مع C++ منذ دراستي الجامعية، لذا قد تكون لغتي غير موجودة هنا، لكن الأفكار العامة تنطبق على معظم لغات OO.

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

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

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

يبدو وكأنه يجب عليك القيام به callFoo وظيفة افتراضية خالصة وإنشاء بعض الفئات الفرعية منها A.

ما لم تكن بحاجة حقًا إلى السرعة، وقمت بإجراء عمليات تحديد ملفات تعريف وأدوات واسعة النطاق، وقررت أن المكالمات إلى callFoo هي حقا عنق الزجاجة.هل؟

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

يجب أن أعتقد أن المؤشر سيكون أسرع.

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

وبطبيعة الحال، يجب عليك قياس كليهما.

تحسين فقط عند الحاجة

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

تعتمد تكلفة المكالمة غير المباشرة على النظام الأساسي المستهدف

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

يمكن للمترجم تنفيذ التبديل باستخدام جدول القفز أيضًا

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

استخدم الموقتات لمعرفة أيهما أسرع.على الرغم من أنه ما لم يتم تكرار هذا الرمز مرارًا وتكرارًا، فمن غير المرجح أن تلاحظ أي فرق.

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

تُستخدم هذه التقنية بكثرة مع نظام التشغيل Symbian OS:http://www.titu.jyu.fi/modpa/Patterns/pattern-TwoPhaseConstruction.html

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

وفي كلتا الحالتين، انظر إلى الكود المجمع لتتأكد من أنه يفعل ما تعتقد أنه يفعله.

إحدى ميزات التبديل التي يتم التغاضي عنها غالبًا (حتى عبر الفرز والفهرسة) هي معرفة أن قيمة معينة تُستخدم في الغالبية العظمى من الحالات.من السهل طلب المفتاح بحيث يتم فحص الأكثر شيوعًا أولاً.

ملاحظة.لتعزيز إجابة جريج، إذا كنت تهتم بالسرعة - قم بالقياس.لا يساعد النظر إلى المجمّع عندما يكون لدى وحدات المعالجة المركزية (CPU) تفرع مسبق/تنبؤي وأكشاك خطوط الأنابيب وما إلى ذلك

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