هل يمكن للمتغيرات المحلية الثابتة تقليص وقت تخصيص الذاكرة؟

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

  •  03-10-2019
  •  | 
  •  

سؤال

لنفترض أن لدي وظيفة في برنامج واحد ملولب يبدو هكذا

void f(some arguments){
    char buffer[32];
    some operations on buffer;
}

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

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

المحلول

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

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

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

التخصيص الديناميكي هو الحالة التي يتم فيها حرق دورات التنفيذ. لكن هذا ليس في نطاق سؤالك.

نصائح أخرى

لا ، إنها ليست تسريعًا مجانيًا.

أولاً ، يكون التخصيص مجانيًا تقريبًا في البدء (لأنه يتكون فقط من إضافة 32 إلى مؤشر المكدس) ، وثانياً ، هناك سببان على الأقل من وجود متغير ثابت أبطأ

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

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

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

الطريقة التي يتم كتابتها بها الآن ، لا توجد تكلفة للتخصيص: البايت 32 على المكدس. العمل الحقيقي الوحيد هو أنك تحتاج إلى صفر.

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

أود أن أقترح أن نهجًا أكثر عمومية لهذه المشكلة هو أنه إذا كان لديك وظيفة تسمى عدة مرات تحتاج إلى بعض المتغيرات المحلية ، ففكر في لفها في الفصل وجعل وظائف أعضاء هذه المتغيرات. ضع في اعتبارك ما إذا كنت بحاجة لجعل الحجم ديناميكيًا ، لذا بدلاً من char buffer[32] عندك std::vector<char> buffer(requiredSize). هذا أغلى من صفيف للتهيئة في كل مرة من خلال الحلقة

class BufferMunger {
public:
   BufferMunger() {};
   void DoFunction(args);
private:
   char buffer[32];
};

BufferMunger m;
for (int i=0; i<1000; i++) {
   m.DoFunction(arg[i]);  // only one allocation of buffer
}

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

لاحظ أن مستوى الكتلة static تتم تهيئة المتغيرات في C ++ (على عكس C) عند الاستخدام الأول. وهذا يعني أن ستقدم تكلفة فحص وقت تشغيل إضافي. من المحتمل أن ينتهي الفرع إلى جعل الأداء أسوأ ، وليس أفضل. (ولكن في الحقيقة ، يجب عليك الملف الشخصي ، كما ذكر آخرون.)

بغض النظر ، لا أعتقد أن الأمر يستحق ذلك ، خاصة وأنك ستضحي عمداً بإعادة الدخول.

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

مثال على الأول: على Z80 ، كان الكود لإعداد إطار المكدس لوظيفة مع أي متغيرات محلية طويلة جدًا. علاوة على ذلك ، اقتصر الكود للوصول إلى المتغيرات المحلية باستخدام وضع العنوان (IX+D) ، والذي كان متاحًا فقط للحصول على تعليمات 8 بت. إذا كان X و Y على حد سواء عالمي/ثابت أو كلاهما المحلي ، فيمكن أن يتم تجميع العبارة "x = y" إما:

; If both are static or global: 6 bytes; 32 cycles
  ld HL,(_Y) ; 16 cycles
  ld (_X),HL ; 16 cycles
; If both are local: 12 bytes; 56 cycles
  ld E,(IX+_Y)   ; 14 cycles
  ld D,(IX+_Y+1) ; 14 cycles
  ld (IX+_X),D   ; 14 cycles
  ld (IX+_X+1),E ; 14 cycles

ركلة جزاء للمساحة بنسبة 100 ٪ وعقوبة الوقت 75 ٪ بالإضافة إلى الكود والوقت لإعداد إطار المكدس!

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

مع دول مجلس التعاون الخليجي ، أرى بعض التسريع:

void f() {
    char buffer[4096];
}

int main() {
    int i;
    for (i = 0; i < 100000000; ++i) {
        f();
    }
}

والوقت:

$ time ./a.out

real    0m0.453s
user    0m0.450s
sys  0m0.010s

تغيير المخزن المؤقت إلى ثابت:

$ time ./a.out

real    0m0.352s
user    0m0.360s
sys  0m0.000s

اعتمادًا على ما يفعله المتغير بالضبط وكيفية استخدامه ، فإن السرعة لا تكاد لا شيء تقريبًا. نظرًا لأن ذاكرة المكدس (على أنظمة X86) يتم تخصيصها لجميع VARs المحلية في نفس الوقت مع FUNC واحد بسيط (Sub ESP ، المبلغ) ، وبالتالي فإن وجود مكدس واحد فقط يلغي أي مكسب. الاستثناء الوحيد لهذا هو المخازن المؤقتة الضخمة حقًا في هذه الحالة التي قد يلتصق بها برنامج التحويل البرمجي في _chkstk لتخصيص الذاكرة (ولكن إذا كان المخزن المؤقت الخاص بك كبيرًا ، فيجب عليك إعادة تقييم الكود الخاص بك). لا يمكن للمترجم تحويل الذاكرة المكدس إلى ذاكرة ثابتة من خلال التحسين ، حيث لا يمكن أن نفترض أن الوظيفة سيتم استخدامها في enviroment ذات الخيوط الواحدة ، بالإضافة إلى أنها ستعبث مع منشئي الكائنات والمدمرة وما إلى ذلك

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

إذا تمت تهيئة متغير ثابت ، فسيكون هناك علامة في مكان ما لتحديد ما إذا كان قد تم بالفعل تهيئة المتغير. سيستغرق التحقق من العلم بعض الوقت. في مثالك ، لا يتم تهيئة المتغير ، بحيث يمكن تجاهل هذا الجزء.

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

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

هذا هو تجاهل أن الإحصائيات لديها العديد من المشاكل الأخرى.

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

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