< أسرع من <= في C++؟ فضح الخرافات مع أمثلة

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

مقدمة: خرافة تعيش لعقود

\n\n

إذا كنت قد ألقيت نظرة على Stack Overflow في قسم C++، فمن المحتمل أنك رأيت السؤال: «هل < أسرع من <=؟». هذا السؤال جمع آلاف المشاهدات وعشرات الصفحات من النقاشات. ولا عجب — موضوع تحسين الكود يثير عقول المبرمجين من جميع المستويات.

\n\n

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

\n\n

اليوم سنحسم هذه الخرافة مرة وإلى الأبد. ستتعرف على:

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

كيف تعمل العوامل < و <= على مستوى لغة التجميع؟

\n\n

لفهم ما إذا كان هناك فرق، يجب أن ننظر «تحت الغطاء» — إلى ما يحول إليه المترجم كود C++ الخاص بك. لننظر إلى دالتين بسيطتين:

\n\n
bool less_than(int a, int b) {\n    return a < b;\n}\n\nbool less_or_equal(int a, int b) {\n    return a <= b;\n}
\n\n

لنقم بتجميعهما باستخدام g++ (بدون تحسينات، العلم -O0). إليك ما سنراه في قائمة لغة التجميع (x86-64):

\n\n
less_than(int, int):\n    cmp    edi, esi      ; مقارنة a و b\n    setl   al            ; تعيين al=1 إذا a < b (أقل من)\n    ret\n\nless_or_equal(int, int):\n    cmp    edi, esi      ; مقارنة a و b\n    setle  al            ; تعيين al=1 إذا a <= b (أقل من أو يساوي)\n    ret
\n\n

هل ترى؟ الفرق الوحيد هو التعليمات setl (تعيين إذا كان أقل) و setle (تعيين إذا كان أقل أو يساوي). كلتا التعليمتين تنفذان في دورة معالج واحدة. على وحدات المعالجة المركزية الحديثة (Intel Core i7، AMD Ryzen)، لهما نفس زمن الوصول — دورة واحدة.

\n\n

الاستنتاج رقم 1: على مستوى تعليمات الآلة، لا يوجد فرق في الأداء بين < و <=. نقطة.

\n\n

وماذا عن الحلقات؟ هنا يجب أن يكون هناك فرق!

\n\n

غالباً ما يدور الجدل حول الحلقات. قارن بين الطريقتين:

\n\n
// الخيار 1: مع <\nfor (int i = 0; i < n; ++i) {\n    // نفعل شيئاً\n}\n\n// الخيار 2: مع <=\nfor (int i = 0; i <= n - 1; ++i) {\n    // نفعل نفس الشيء\n}
\n\n

الحدس يخبرنا: الخيار الثاني يقوم بحساب إضافي n - 1. لكن دعنا نرى ما يحدث بعد التحسين من قبل المترجم.

\n\n

لنقم بتشغيل معيار مع g++ -O2 لـ n = 1 000 000 000:

\n\n
#include <iostream>\n#include <chrono>\n\nint main() {\n    const long long n = 1'000'000'000;\n    volatile long long sink = 0; // لمنع المترجم من إزالة الحلقة\n\n    auto start = std::chrono::high_resolution_clock::now();\n    \n    for (long long i = 0; i < n; ++i) {\n        sink += i;\n    }\n    \n    auto end = std::chrono::high_resolution_clock::now();\n    std::cout << "الوقت (<): " \n              << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()\n              << " ms\\n";\n\n    start = std::chrono::high_resolution_clock::now();\n    \n    for (long long i = 0; i <= n - 1; ++i) {\n        sink += i;\n    }\n    \n    end = std::chrono::high_resolution_clock::now();\n    std::cout << "الوقت (<=): " \n              << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()\n              << " ms\\n";\n\n    return 0;\n}
\n\n

النتائج على جهازي (i7-107

المدونات

توصيات الكتب