مقدمة: خرافة تعيش لعقود
\n\nإذا كنت قد ألقيت نظرة على Stack Overflow في قسم C++، فمن المحتمل أنك رأيت السؤال: «هل < أسرع من <=؟». هذا السؤال جمع آلاف المشاهدات وعشرات الصفحات من النقاشات. ولا عجب — موضوع تحسين الكود يثير عقول المبرمجين من جميع المستويات.
\n\nيبدو المنطق قوياً: العامل < يقارن رقمين ويعيد النتيجة، بينما <= يفعل نفس الشيء ولكنه يتحقق أيضاً من المساواة. لذا، يجب أن يكون < أسرع، أليس كذلك؟ هذا مهم بشكل خاص في الحلقات، حيث يتم تنفيذ عملية المقارنة ملايين المرات.
اليوم سنحسم هذه الخرافة مرة وإلى الأبد. ستتعرف على:
\n- \n
- كيف تعمل العوامل فعلياً على مستوى المعالج. \n
- لماذا المترجم هو أفضل صديق لك (أو عدوك، إذا كنت لا تثق به). \n
- متى يكون التحسين الدقيق منطقياً، ومتى يكون مضيعة للوقت. \n
كيف تعمل العوامل < و <= على مستوى لغة التجميع؟
\n\nلفهم ما إذا كان هناك فرق، يجب أن ننظر «تحت الغطاء» — إلى ما يحول إليه المترجم كود C++ الخاص بك. لننظر إلى دالتين بسيطتين:
\n\nbool 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):
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)، لهما نفس زمن الوصول — دورة واحدة.
الاستنتاج رقم 1: على مستوى تعليمات الآلة، لا يوجد فرق في الأداء بين < و <=. نقطة.
وماذا عن الحلقات؟ هنا يجب أن يكون هناك فرق!
\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. لكن دعنا نرى ما يحدث بعد التحسين من قبل المترجم.
لنقم بتشغيل معيار مع 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