سؤال

ما هي أفضل الممارسات لاستخدام أ switch بيان مقابل استخدام if بيان لمدة 30 unsigned التعدادات حيث يوجد حوالي 10 بها إجراء متوقع (وهذا هو نفس الإجراء حاليًا).يجب مراعاة الأداء والمساحة ولكنها ليست حاسمة.لقد قمت بتلخيص المقتطف لذا لا تكرهني بسبب اصطلاحات التسمية.

switch إفادة:

// numError is an error enumeration type, with 0 being the non-error case
// fire_special_event() is a stub method for the shared processing

switch (numError)
{  
  case ERROR_01 :  // intentional fall-through
  case ERROR_07 :  // intentional fall-through
  case ERROR_0A :  // intentional fall-through
  case ERROR_10 :  // intentional fall-through
  case ERROR_15 :  // intentional fall-through
  case ERROR_16 :  // intentional fall-through
  case ERROR_20 :
  {
     fire_special_event();
  }
  break;

  default:
  {
    // error codes that require no additional action
  }
  break;       
}

if إفادة:

if ((ERROR_01 == numError)  ||
    (ERROR_07 == numError)  ||
    (ERROR_0A == numError)  || 
    (ERROR_10 == numError)  ||
    (ERROR_15 == numError)  ||
    (ERROR_16 == numError)  ||
    (ERROR_20 == numError))
{
  fire_special_event();
}
هل كانت مفيدة؟

المحلول

استخدم التبديل.

في أسوأ الحالات، سيقوم المترجم بإنشاء نفس التعليمات البرمجية لسلسلة if-else، لذلك لن تفقد أي شيء.إذا كنت في شك، ضع الحالات الأكثر شيوعًا أولاً في بيان التبديل.

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

نصائح أخرى

بالنسبة للحالة الخاصة التي قدمتها في المثال الخاص بك، فمن المحتمل أن يكون الرمز الأوضح هو:

if (RequiresSpecialEvent(numError))
    fire_special_event();

من الواضح أن هذا ينقل المشكلة إلى منطقة مختلفة من الكود، ولكن الآن لديك الفرصة لإعادة استخدام هذا الاختبار.لديك أيضًا المزيد من الخيارات لكيفية حلها.يمكنك استخدام std::set، على سبيل المثال:

bool RequiresSpecialEvent(int numError)
{
    return specialSet.find(numError) != specialSet.end();
}

أنا لا أقترح أن هذا هو أفضل تطبيق لـ RequiresSpecialEvent، ولكنه مجرد خيار.لا يزال بإمكانك استخدام مفتاح التبديل أو سلسلة if-else، أو جدول البحث، أو بعض التلاعب بالبت على القيمة، أيًا كان.كلما أصبحت عملية اتخاذ القرار الخاصة بك أكثر غموضًا، زادت القيمة التي تستمدها من وجودها في وظيفة معزولة.

مفتاح يكون أسرع.

ما عليك سوى تجربة if/else-ing 30 قيمة مختلفة داخل الحلقة، ومقارنتها بنفس الكود باستخدام المفتاح Switch لمعرفة مدى سرعة التبديل.

الآن، التبديل لديه مشكلة حقيقية واحدة :يجب أن يعرف المحول في وقت الترجمة القيم الموجودة داخل كل حالة.وهذا يعني أن الكود التالي:

// WON'T COMPILE
extern const int MY_VALUE ;

void doSomething(const int p_iValue)
{
    switch(p_iValue)
    {
       case MY_VALUE : /* do something */ ; break ;
       default : /* do something else */ ; break ;
    }
}

لن تجميع.

سيستخدم معظم الأشخاص بعد ذلك التعريفات (Aargh!)، وسيقوم الآخرون بإعلان وتحديد المتغيرات الثابتة في نفس وحدة الترجمة.على سبيل المثال:

// WILL COMPILE
const int MY_VALUE = 25 ;

void doSomething(const int p_iValue)
{
    switch(p_iValue)
    {
       case MY_VALUE : /* do something */ ; break ;
       default : /* do something else */ ; break ;
    }
}

لذلك، في النهاية، يجب على المطور الاختيار بين "السرعة + الوضوح" مقابل "السرعة + الوضوح"."اقتران الكود".

(لا يعني ذلك أن المفتاح لا يمكن كتابته ليكون مربكًا مثل الجحيم ...معظم المفاتيح التي أراها حاليًا تنتمي إلى هذه الفئة "المربكة""..."ولكن هذه قصة أخرى...)

تحرير 2008-09-21:

bk1e أضاف التعليق التالي:"يعد تعريف الثوابت على أنها تعدادات في ملف رأس طريقة أخرى للتعامل مع هذا الأمر".

بالطبع هو كذلك.

كان الهدف من النوع الخارجي هو فصل القيمة عن المصدر.تحديد هذه القيمة على أنها ماكرو، أو كإعلان بسيط لـ const int، أو حتى كتعداد له تأثير جانبي يتمثل في تضمين القيمة.وبالتالي، في حالة تغيير التعريف أو قيمة التعداد أو قيمة const int، ستكون هناك حاجة إلى إعادة الترجمة.الإعلان الخارجي يعني أنه ليست هناك حاجة لإعادة الترجمة في حالة تغيير القيمة، ولكن من ناحية أخرى، يجعل من المستحيل استخدام التبديل.الاستنتاج هو سيؤدي استخدام التبديل إلى زيادة الاقتران بين رمز التبديل والمتغيرات المستخدمة كحالات.عندما يكون الأمر على ما يرام، استخدم المفتاح.عندما لا يكون الأمر كذلك، فلا مفاجأة.

.

تحرير 15-01-2013:

فلاد لازارينكو علق على إجابتي، معطيًا رابطًا لدراسته المتعمقة لرمز التجميع الناتج عن المحول.المنير جدا: http://741mhz.com/switch/

سيقوم المترجم بتحسينه على أي حال - انتقل إلى المفتاح لأنه الأكثر قابلية للقراءة.

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

خطأ_01 :// السقوط المتعمد

أو

(ERROR_01 == numError) ||

الأحدث أكثر عرضة للخطأ ويتطلب كتابة وتنسيقًا أكثر من الأول.

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

استخدم التبديل، فهو الغرض منه وما يتوقعه المبرمجون.

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

المترجمون جيدون حقًا في التحسين switch.تعتبر دول مجلس التعاون الخليجي الأخيرة جيدة أيضًا في تحسين مجموعة من الظروف في if.

لقد قمت ببعض حالات الاختبار على godbolt.

عندما case يتم تجميع القيم بشكل قريب من بعضها البعض، وتكون كل من gcc و clang و icc ذكية بما يكفي لاستخدام صورة نقطية للتحقق مما إذا كانت القيمة واحدة من القيم الخاصة.

على سبيل المثاليقوم gcc 5.2 -O3 بتجميع ملف switch إلى (و if شيء مشابه جدًا):

errhandler_switch(errtype):  # gcc 5.2 -O3
    cmpl    $32, %edi
    ja  .L5
    movabsq $4301325442, %rax   # highest set bit is bit 32 (the 33rd bit)
    btq %rdi, %rax
    jc  .L10
.L5:
    rep ret
.L10:
    jmp fire_special_event()

لاحظ أن الصورة النقطية عبارة عن بيانات فورية، لذلك لا يوجد احتمال لفقد ذاكرة التخزين المؤقت للبيانات عند الوصول إليها، أو جدول الانتقال.

gcc 4.9.2 -O3 يجمع ملف switch إلى صورة نقطية، ولكن لا 1U<<errNumber مع موف/التحول.يقوم بتجميع if نسخة إلى سلسلة من الفروع.

errhandler_switch(errtype):  # gcc 4.9.2 -O3
    leal    -1(%rdi), %ecx
    cmpl    $31, %ecx    # cmpl $32, %edi  wouldn't have to wait an extra cycle for lea's output.
              # However, register read ports are limited on pre-SnB Intel
    ja  .L5
    movl    $1, %eax
    salq    %cl, %rax   # with -march=haswell, it will use BMI's shlx to avoid moving the shift count into ecx
    testl   $2150662721, %eax
    jne .L10
.L5:
    rep ret
.L10:
    jmp fire_special_event()

لاحظ كيف يطرح 1 من errNumber (مع lea لدمج تلك العملية مع الحركة).يتيح ذلك احتواء الصورة النقطية في صورة 32 بت فورية، وتجنب الصورة النقطية 64 بت الفورية movabsq الذي يأخذ المزيد من بايت التعليمات.

سيكون التسلسل الأقصر (في رمز الجهاز) هو:

    cmpl    $32, %edi
    ja  .L5
    mov     $2150662721, %eax
    dec     %edi   # movabsq and btq is fewer instructions / fewer Intel uops, but this saves several bytes
    bt     %edi, %eax
    jc  fire_special_event
.L5:
    ret

( الفشل في الاستخدام jc fire_special_event هو في كل مكان، وهو خطأ مترجم.)

rep ret يستخدم في أهداف الفروع، وبعد الفروع الشرطية، لصالح AMD K8 وK10 القديم (ما قبل البلدوزر): ماذا يعني "ممثل"؟.بدونها، لن يعمل التنبؤ بالفروع بشكل جيد على وحدات المعالجة المركزية (CPUs) القديمة.

bt (اختبار البت) مع وسيطة التسجيل سريع.فهو يجمع بين عمل التحول إلى اليسار بمقدار 1 errNumber بت والقيام test, ، ولكن لا يزال هناك زمن استجابة لدورة واحدة ومعالج Intel uop واحد فقط.إنه بطيء مع وسيطة الذاكرة بسبب دلالات CISC الخاصة به:باستخدام معامل ذاكرة لـ "سلسلة البتات"، يتم حساب عنوان البايت المراد اختباره استنادًا إلى الوسيطة الأخرى (مقسمة على 8)، ولا يقتصر على القطعة المكونة من 1 أو 2 أو 4 أو 8 بايت المشار إليها بواسطة معامل الذاكرة.

من جداول تعليمات Agner Fog, ، تعليمات التحول ذات العدد المتغير أبطأ من أ bt على Intel الحديثة (2 uops بدلاً من 1، ولا يقوم Shift بكل ما هو مطلوب).

IMO يعد هذا مثالًا مثاليًا لما تم إجراء التبديل من أجله.

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

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

أفضّل عبارات if على عبارات الحالة لأنها أكثر قابلية للقراءة وأكثر مرونة - يمكنك إضافة شروط أخرى لا تعتمد على المساواة الرقمية، مثل " || max < min ".لكن بالنسبة للحالة البسيطة التي نشرتها هنا، فلا يهم حقًا، فقط افعل ما هو أكثر قابلية للقراءة بالنسبة لك.

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

الازدواجية في if الحالة صعبة على العيون.لنفترض واحدا من == كتب !=;هل ستلاحظ؟أو إذا تمت كتابة مثيل واحد لـ "numError" بـ "nmuError"، والذي تم تجميعه للتو؟

أفضّل عمومًا استخدام تعدد الأشكال بدلاً من التبديل، ولكن بدون مزيد من التفاصيل حول السياق، من الصعب تحديد ذلك.

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

أنا أتفق مع مدى توافق حل التبديل ولكن IMO أنت كذلك اختطاف التبديل هنا.
والغرض من التبديل هو أن يكون مختلف التعامل اعتمادا على القيمة.
إذا كان عليك أن تشرح خوارزميتك برمز زائف، فستستخدم if لأنه، من الناحية الدلالية، هذا هو ما هو عليه: إذا كان مهما_خطأ افعل هذا...
لذا، ما لم تكن تنوي يومًا ما تغيير الكود الخاص بك للحصول على كود محدد لكل خطأ، فسأستخدمه لو.

لست متأكدًا من أفضل الممارسات، ولكني سأستخدم التبديل - ثم اعترض السقوط المتعمد عبر "الافتراضي"

من الناحية الجمالية، أميل إلى تفضيل هذا النهج.

unsigned int special_events[] = {
    ERROR_01,
    ERROR_07,
    ERROR_0A,
    ERROR_10,
    ERROR_15,
    ERROR_16,
    ERROR_20
 };
 int special_events_length = sizeof (special_events) / sizeof (unsigned int);

 void process_event(unsigned int numError) {
     for (int i = 0; i < special_events_length; i++) {
         if (numError == special_events[i]) {
             fire_special_event();
             break;
          }
     }
  }

اجعل البيانات أكثر ذكاءً قليلاً حتى نتمكن من جعل المنطق أكثر غباءً قليلاً.

أدرك أنه يبدو غريبا.إليك الإلهام (من كيفية القيام بذلك في بايثون):

special_events = [
    ERROR_01,
    ERROR_07,
    ERROR_0A,
    ERROR_10,
    ERROR_15,
    ERROR_16,
    ERROR_20,
    ]
def process_event(numError):
    if numError in special_events:
         fire_special_event()
while (true) != while (loop)

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

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

أنا لست الشخص الذي يخبرك عن السرعة واستخدام الذاكرة، ولكن النظر إلى بيان التبديل يعد أسهل بكثير في الفهم من بيان if الكبير (خاصة بعد 2-3 أشهر)

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

switch(numerror){
    ERROR_20 : { fire_special_event(); } break;
    default : { null; } break;
}

ربما حتى تختبر حالتك (في هذه الحالة خطأ رقمي) مقابل قائمة من الاحتمالات، أو مصفوفة ربما حتى لا يتم استخدام SWITCH الخاص بك حتى ما لم تكن هناك نتيجة بالتأكيد.

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

أعرف أنها قديمة ولكن

public class SwitchTest {
static final int max = 100000;

public static void main(String[] args) {

int counter1 = 0;
long start1 = 0l;
long total1 = 0l;

int counter2 = 0;
long start2 = 0l;
long total2 = 0l;
boolean loop = true;

start1 = System.currentTimeMillis();
while (true) {
  if (counter1 == max) {
    break;
  } else {
    counter1++;
  }
}
total1 = System.currentTimeMillis() - start1;

start2 = System.currentTimeMillis();
while (loop) {
  switch (counter2) {
    case max:
      loop = false;
      break;
    default:
      counter2++;
  }
}
total2 = System.currentTimeMillis() - start2;

System.out.println("While if/else: " + total1 + "ms");
System.out.println("Switch: " + total2 + "ms");
System.out.println("Max Loops: " + max);

System.exit(0);
}
}

تغيير عدد الحلقات يتغير كثيرًا:

بينما إذا/إلا:مفتاح 5 مللي ثانية:1MS Max Loops:100000

بينما إذا/إلا:مفتاح 5 مللي ثانية:3MS Max Loops:1000000

بينما إذا/إلا:مفتاح 5 مللي ثانية:14ms Max Loops:10000000

بينما إذا/إلا:مفتاح 5 مللي ثانية:149ms Max Loops:100000000

(أضف المزيد من البيانات إذا كنت تريد)

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

اسمح لك باختبار متغير مقابل نطاقات محددة ، يمكنك استخدام وظائف (المكتبة القياسية أو الشخصية) كشرطي.

(مثال:

`int a;
 cout<<"enter value:\n";
 cin>>a;

 if( a > 0 && a < 5)
   {
     cout<<"a is between 0, 5\n";

   }else if(a > 5 && a < 10)

     cout<<"a is between 5,10\n";

   }else{

       "a is not an integer, or is not in range 0,10\n";

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

`int a;
 cout<<"enter value:\n";
 cin>>a;

 switch(a)
 {
    case 0:
    case 1:
    case 2: 
    case 3:
    case 4:
    case 5:
        cout<<"a is between 0,5 and equals: "<<a<<"\n";
        break;
    //other case statements
    default:
        cout<<"a is not between the range or is not a good value\n"
        break;

أفضّل عبارات if - else if - else، لكن الأمر متروك لك حقًا.إذا كنت تريد استخدام الدوال كشروط، أو تريد اختبار شيء ما مقابل نطاق أو مصفوفة أو ناقل و/أو لا تمانع في التعامل مع التداخل المعقد، فإنني أوصي باستخدام كتل If else if else.إذا كنت تريد الاختبار مقابل قيم مفردة أو كنت تريد كتلة نظيفة وسهلة القراءة، فإنني أنصحك باستخدام كتل الحالة الخاصة بـ Switch().

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