0.1f مقابل 0: لماذا يبطئ كود C++ بمقدار 10 أضعاف وكيفية إصلاح ذلك

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

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

ابدأ الدورة

مقدمة: خطأ كلف 10 أضعاف الأداء

\n\n

تخيل الموقف: أنت تكتب كود C++ عالي الأداء. كل شيء يعمل بسرعة، الاختبارات تمر، وتشعر وكأنك عبقري التحسين. ثم تقوم بتغيير ثابت واحد فقط — 0.1f إلى 0. وينخفض الأداء بمقدار 10 أضعاف. بدون مزاح. بدون مبالغة. هذا السؤال بالتحديد حصل على آلاف المشاهدات على Stack Overflow وأصبح كلاسيكياً لمن يريد فهم كيفية عمل أعداد الفاصلة العائمة على مستوى المعالج.

\n\n

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

\n\n

جوهر المشكلة: الأعداد غير المعيارية وتأثيرها على أداء C++

\n\n

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

\n\n
#include <iostream>\n#include <chrono>\n\nvoid slow_function(float factor) {\n    float sum = 0.0f;\n    for (int i = 0; i < 100000000; ++i) {\n        sum += factor * 0.1f;  // سريع إذا كان factor = 0.1f\n        // sum += factor * 0;  // بطيء إذا كان factor = 0\n    }\n    std::cout << sum << std::endl;\n}\n\nint main() {\n    auto start = std::chrono::high_resolution_clock::now();\n    slow_function(0.1f);  // جرب استبدالها بـ 0\n    auto end = std::chrono::high_resolution_clock::now();\n    std::cout << "Time: " \n              << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() \n              << " ms" << std::endl;\n    return 0;\n}
\n\n

إذا قمت بتشغيل هذا الكود مع factor = 0.1f، سيكون وقت التنفيذ، لنقل، 100 مللي ثانية. استبدلها بـ factor = 0 — وستحصل على 1000 مللي ثانية أو أكثر. الفرق 10 أضعاف. لماذا؟

\n\n

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

\n\n

ما هي الأعداد غير المعيارية ولماذا تبطئ الكود؟

\n\n

معيار IEEE 754 لأعداد الفاصلة العائمة لا يحدد فقط الأعداد المعيارية (حيث يبدأ الجزء العشري بـ 1)، بل يحدد أيضاً الأعداد غير المعيارية (subnormal). تُستخدم لتمثيل قيم صغيرة جداً قريبة من الصفر. عندما تصبح نتيجة الحسابات أصغر من أصغر عدد معياري، ينتقل المعالج إلى وضع معالجة الأعداد غير المعيارية.

\n\n

المشكلة هي أن الدعم العتادي للأعداد غير المعيارية في معالجات x86 بطيء جداً. إذا كانت العمليات العادية على float تستغرق 1-3 دورات ساعة، فإن العمليات على الأعداد غير المعيارية قد تستغرق 100-200 دورة أو أكثر. هذا هو سبب انخفاض الأداء.

\n\n

لنعد إلى مثالنا. عندما يكون factor = 0.1f، تبقى جميع النتائج الوسيطة في النطاق المعياري. ولكن عندما يكون factor = 0، فإن الضرب في صفر يعطي صفراً. ثم نضيف 0.1f إلى الصفر — نحصل على 0.1f. لكن في التكرار التالي، نضرب مرة أخرى في صفر ونحصل مرة أخرى على صفر. وهكذا ملايين المرات. نتيجة لذلك، يولد المعالج باستمرار قيماً غير معيارية، مما يؤدي إلى تباطؤ كارثي.

\n\n

تجربة: كيف ترى الفرق بنفسك

\n\n

لنكتب مثالاً أكثر وضوحاً يوضح كيف تؤثر الأعداد غير المعيارية على أداء كود C++:

\n\n
#include <iostream>\n#include <chrono>\n#include <cmath>\n\nvoid test_performance(float initial_value) {\n    float x = initial_value;\n    auto start = std::chrono::high_resolution_clock::now();\n    \n    for (int i = 0; i < 100000000; ++i) {\n        x = x * 0.5f + 0.1f;  // صيغة تكرارية نموذجية\n    }\n    \n                

المدونات

توصيات الكتب