أعطى أحد مستخدمي برنامج التحويل البرمجي Visual C ++ مثال الكود التالي وسأل عن سبب تنفيذ حلقته مع الشرط إلى ما لا نهاية ، على الرغم من أن الشرط يجب أن يتوقف عند نقطة ما ويجب أن تنتهي الدورة:
#include <windows.h> int x = 0, y = 1; int* ptr; DWORD CALLBACK ThreadProc(void*) { Sleep(1000); ptr = &y; return 0; } int main(int, char**) { ptr = &x; // starts out pointing to x DWORD id; HANDLE hThread = CreateThread(nullptr, 0, ThreadProc, 0, &id); // , ptr // while (*ptr == 0) { } return 0; }
بالنسبة لأولئك الذين ليسوا على دراية بالميزات الخاصة بمنصة Windows ، فإليك المعادل في C ++ النقي:
#include <chrono> #include <thread> int x = 0, y = 1; int* ptr = &x; void ThreadProc() { std::this_thread::sleep_for(std::chrono::seconds(1)); ptr = &y; } int main(int, char**) { ptr = &x; // starts out pointing to x std::thread thread(ThreadProc); // , ptr // while (*ptr == 0) { } return 0; }
بعد ذلك ، جلب المستخدم فهمه للبرنامج:
تم تحويل الحلقة الشرطية إلى لانهائي من قبل المترجم. أرى ذلك من كود المجمع الذي تم تحميله ، والذي بمجرد تحميل قيمة مؤشر ptr في السجل (في بداية الحلقة) ، ثم يقارن قيمة هذا السجل بصفر عند كل تكرار. نظرًا لأن إعادة تحميل القيمة من ptr لا تحدث أبدًا مرة أخرى ، فإن الدورة لا تنتهي أبدًا.
أنا أفهم أن الإعلان عن ptr على أنه "متغير int *" يجب أن يؤدي إلى إسقاط التحسينات للمترجم وقراءة قيمة ptr في كل تكرار للحلقة ، مما سيحل المشكلة. ولكن ما زلت أود أن أعرف لماذا لا يمكن للمترجم أن يكون ذكيًا بما يكفي للقيام بهذه الأشياء تلقائيًا؟ من الواضح أن المتغير العام المستخدم في خيطين مختلفين يمكن تغييره ، مما يعني أنه لا يمكن تخزينه مؤقتًا في السجل. لماذا لا يستطيع المترجم إنشاء الكود الصحيح على الفور؟
قبل الإجابة على هذا السؤال ، دعنا نبدأ بقليل من الالتقاط: "لا يشير المتغير int * ptr" إلى متغير ptr على أنه "مؤشر يحظر إجراء تحسينات عليه". هذا "مؤشر عادي لمتغير ممنوع إجراء التحسينات عليه." ما كان يفكر فيه مؤلف السؤال أعلاه هو أن يتم الإعلان عنه على أنه "int * volatile ptr".
الآن نعود إلى السؤال الرئيسي. ما الذي يحدث هنا؟
حتى نظرة خاطفة على الكود ستخبرنا أنه لا توجد متغيرات مثل std :: atomic ، ولا استخدام std :: memory_order (إما صريحًا أو ضمنيًا). وهذا يعني أن أي محاولة للوصول إلى ptr أو * ptr من دفقين مختلفين تؤدي إلى سلوك غير محدد. بشكل حدسي ، يمكنك التفكير في الأمر بهذه الطريقة: "المحول البرمجي يحسن كل موضوع كما لو كان يعمل بمفرده في البرنامج. النقاط الوحيدة التي يجب أن يفكر فيها المترجم في الوصول إلى البيانات من تدفقات مختلفة هي استخدام std :: atomic أو std :: memory_order. "
وهذا يفسر سبب عدم تصرف البرنامج كما توقع المبرمج. منذ اللحظة التي تسمح فيها بسلوك غامض - لا يمكن ضمان أي شيء على الإطلاق.
ولكن حسنًا ، دعنا نفكر في الجزء الثاني من سؤاله: لماذا لا يكون المترجم ذكيًا بما يكفي للتعرف على هذا الموقف وإيقاف التحسين تلقائيًا عن طريق تحميل قيمة المؤشر في السجل؟ حسنًا ، يطبق المترجم تلقائيًا كل ما هو ممكن ولا يتعارض مع معيار التحسين. سيكون من الغريب أن نطلب منه أن يكون قادرًا على قراءة أفكار المبرمج وتعطيل بعض التحسينات التي لا تتعارض مع المعيار ، والتي ، ربما ، وفقًا للمبرمج يجب أن تغير منطق البرنامج للأفضل. "ماذا لو توقعت هذه الدورة تغيرًا في قيمة متغير عام في سلسلة رسائل أخرى ، على الرغم من أنه لم يتم الإعلان عنه صراحة؟ سأستغرق الأمر مائة مرة لإبطائه لتكون مستعدًا لهذا الموقف! " هل يجب أن يكون الأمر كذلك؟ بالكاد.
ولكن لنفترض أننا أضفنا قاعدة إلى المترجم مثل "إذا أدى التحسين إلى ظهور حلقة لا نهائية ، فأنت بحاجة إلى إلغائها وجمع الشفرة بدون تحسين". أو حتى مثل هذا: "قم بإلغاء التحسينات الفردية بنجاح حتى تكون النتيجة حلقة غير منتهية." إلى جانب المفاجآت المذهلة التي سيجلبها هذا ، هل سيعطي أي فائدة على الإطلاق؟
نعم ، في هذه الحالة النظرية لن نحصل على حلقة لا نهائية. ستتم مقاطعته إذا كتب دفق آخر قيمة غير صفرية إلى * ptr. سيتم مقاطعته أيضًا إذا كتب مؤشر ترابط آخر قيمة غير صفرية إلى المتغير x. لا يتضح مدى عمق تحليل التبعيات من أجل "التقاط" جميع الحالات التي قد تؤثر على الوضع. نظرًا لأن المترجم لا يقوم بالفعل بتشغيل البرنامج الذي تم إنشاؤه ولا يحلل سلوكه في وقت التشغيل ، فإن المخرج الوحيد هو افتراض أنه لا يمكن تحسين أي مكالمات للمتغيرات والمؤشرات والروابط على الإطلاق.
int limit; void do_something() { ... if (value > limit) value = limit;
هذا يتعارض تمامًا مع روح C ++. يقول معيار اللغة أنه إذا قمت بتعديل متغير وتوقعت رؤية هذا التعديل في مؤشر ترابط آخر ، يجب أن تقول صراحة هذا: استخدم عملية ذرية أو تنظيم الوصول إلى الذاكرة (عادة باستخدام كائن التزامن).
لذا يرجى القيام بذلك.