ما هي سجلات وحدة المعالجة المركزية وكيف يتم استخدامها ، وخاصة WRT Multithreading؟

StackOverflow https://stackoverflow.com/questions/2384578

سؤال

هذا السؤال وإجابتي أدناه هي أساسا ردًا على منطقة ارتباك في سؤال آخر.

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

هل كانت مفيدة؟

المحلول

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

كل ما يتم تشغيله يستخدم السجلات ويمتلك مؤشر التعليمات (الذي يقول التعليمات التالية). عندما يتبادل نظام التشغيل في موضوع آخر ، يتم حفظ جميع حالة وحدة المعالجة المركزية ، بما في ذلك السجلات ومؤشر التعليمات ، في مكان ما ، وتجفيفًا فعليًا لحالة الخيط عندما يعود إلى الحياة التالية.

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

نصائح أخرى

السجلات هي "تخزين العمل" في وحدة المعالجة المركزية. إنها سريعة جدًا ، لكنها مورد محدود للغاية. عادةً ما تحتوي وحدة المعالجة المركزية على مجموعة صغيرة ثابتة من السجلات المسماة ، والأسماء التي تكون جزءًا من اتفاقية لغة التجميع لرمز آلة وحدات المعالجة المركزية. على سبيل المثال ، تحتوي وحدات المعالجة المركزية 32-bit Intel X86 على أربعة سجلات بيانات رئيسية تسمى EAX و EBX و ECX و EDX ، إلى جانب عدد من السجلات الفهرسة وغيرها من السجلات الأكثر تخصصًا.

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

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

mov eax, [var1]
add eax, [var2]
mov [var1], eax

IIRC ، وهذا صحيح (على الرغم من أنه غير فعال) رمز تجميع x86. على موتورولا 68000 ، قد أكتب ...

move.l [var1], d0
add.l  [var2], d0
move.l d0, [var1]

هذه المرة ، عادة ما يكون المصدر هو المعلمة اليسرى ، مع الوجهة على اليمين. كان لدى 68000 سجلات بيانات 8 سجلات (D0..D7) و 8 سجلات عناوين (A0..A7) ، مع A7 IIRC أيضًا كمؤشر مكدس.

في 6510 (مرة أخرى على العميد القديم الجيد 64) قد أكتب ...

lda    var1
adc    var2
sta    var1

السجلات هنا في الغالب ضمنية في التعليمات - كل ما سبق استخدام سجل (تراكم).

أرجو أن تسامح أي أخطاء سخيفة في هذه الأمثلة - لم أكتب أي قدر كبير من التجميع "الحقيقي" (وليس الافتراضي) لمدة 15 عامًا على الأقل. المبدأ هو النقطة ، على الرغم من.

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

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

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

عادة ، يتم تخيل المتغيرات المحلية في وظيفة للعيش على المكدس. هذه هي القاعدة العامة مع متغيرات "Auto" في C. نظرًا لأن "Auto" هو الافتراضي ، فهذه هي المتغيرات المحلية العادية. علي سبيل المثال...

void myfunc ()
{
  int i;  //  normal (auto) local variable
  //...
  nested_call ();
  //...
}

في الكود أعلاه ، يمكن عقد "أنا" في المقام الأول في السجل. قد يتم نقله من سجل إلى آخر والعودة مع تقدم الوظيفة. ومع ذلك ، عندما يتم استدعاء "nested_call" ، ستكون القيمة من هذا السجل بالتأكيد على المكدس - إما لأن المتغير هو متغير مكدس (وليس سجلًا) ، أو بسبب حفظ محتويات التسجيل للسماح بتخزين العمل الخاص به nested_call .

في تطبيق Multithreading ، تكون المتغيرات المحلية العادية محلية لخيط معين. يحصل كل مؤشر ترابط على مكدس خاص به ، وبينما يتم تشغيله ، الاستخدام الحصري لسجلات وحدة المعالجة المركزية. في مفتاح السياق ، يتم حفظ هذه السجلات. سواء في السجلات أو على المكدس ، لا تتم مشاركة المتغيرات المحلية بين المواضيع.

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

تتطلب البيانات المخزنة في الذاكرة المشتركة المزيد من الرعاية. ويشمل ذلك المتغيرات العالمية ، والمتغيرات الثابتة داخل كل من الفئتين والوظائف ، والكائنات المخصصة للكومة. علي سبيل المثال...

void myfunc ()
{
  static int i;  //  static variable
  //...
  nested_call ();
  //...
}

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

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

هذا يعني أن التعديلات التي تم إجراؤها في موضوع واحد قد لا يتم رؤيتها بواسطة موضوع آخر لبعض الوقت. يمكن أن ينتهي الأمر بموضوعين بأفكار مختلفة جدًا حول قيمة "أنا" أعلاه.

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

الحل الجزئي هو الإبلاغ عن متغير مثل "متقلبة" ...

void myfunc ()
{
  volatile static int i;
  //...
  nested_call ();
  //...
}

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

هذا هو ليس حل المزامنة متعددة القراءة ، ومع ذلك - على الأقل ليس في حد ذاته. يتمثل أحد الحلول المتعددة المناسبة في استخدام نوع من القفل لإدارة الوصول إلى هذا "المورد المشترك". علي سبيل المثال...

void myfunc ()
{
  static int i;
  //...
  acquire_lock_on_i ();
  //  do stuff with i
  release_lock_on_i ();
  //...
}

هناك ما يحدث هنا أكثر مما هو واضح على الفور. من حيث المبدأ ، بدلاً من كتابة قيمة "I" إلى متغيرها جاهزًا لمكالمة "relex_lock_on_i" ، يمكن حفظها على المكدس. بقدر ما يتعلق الأمر بالمترجم ، هذا ليس غير معقول. إنه يقوم بالوصول إلى المكدس على أي حال (على سبيل المثال حفظ عنوان الإرجاع) ، لذلك قد يكون حفظ السجل على المكدس أكثر كفاءة من كتابته إلى "I" - أكثر ملاءمة من ذاكرة التخزين المؤقت من الوصول إلى كتلة منفصلة تمامًا من الذاكرة.

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

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

لحل هذا ، يمكننا إعادة "المتقلبة".

void myfunc ()
{
  volatile static int i;
  //...
  acquire_lock_on_i ();
  //  do stuff with i
  release_lock_on_i ();
  //...
}

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

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

ومع ذلك ، فإن مشكلة حاجز الذاكرة هذه لا تزيل الحاجة إلى الكلمة الرئيسية "المتقلبة".

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