كيف تعمل وحدات الماكرو المحتملة/غير المحتملة في Linux kernel وما هي فائدتها؟

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

سؤال

لقد قمت بالتنقيب في بعض أجزاء Linux kernel، ووجدت مكالمات مثل هذا:

if (unlikely(fd < 0))
{
    /* Do something */
}

أو

if (likely(!err))
{
    /* Do something */
}

لقد وجدت تعريفا لهم:

#define likely(x)       __builtin_expect((x),1)
#define unlikely(x)     __builtin_expect((x),0)

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

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

المحلول

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

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

نصائح أخرى

هذه هي وحدات الماكرو التي تعطي تلميحات للمترجم حول الطريقة التي قد يسلكها الفرع.تتوسع وحدات الماكرو لتشمل امتدادات محددة لدول مجلس التعاون الخليجي، إذا كانت متوفرة.

يستخدم مجلس التعاون الخليجي هذه لتحسين التنبؤ بالفروع.على سبيل المثال، إذا كان لديك شيء مثل ما يلي

if (unlikely(x)) {
  dosomething();
}

return x;

وبعد ذلك يمكنه إعادة هيكلة هذا الكود ليكون شيئًا أشبه بما يلي:

if (!x) {
  return x;
}

dosomething();
return x;

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

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

هناك عدد من الاستراتيجيات الأخرى التي يمكن للمترجم والمعالج استخدامها في هذه السيناريوهات.يمكنك العثور على مزيد من التفاصيل حول كيفية عمل المتنبئين بالفروع في ويكيبيديا: http://en.wikipedia.org/wiki/Branch_predictor

دعونا نقوم بفك الترجمة لنرى ما يفعله إصدار مجلس التعاون الخليجي 4.8 به

بدون __builtin_expect

#include "stdio.h"
#include "time.h"

int main() {
    /* Use time to prevent it from being optimized away. */
    int i = !time(NULL);
    if (i)
        printf("%d\n", i);
    puts("a");
    return 0;
}

الترجمة والتفكيك باستخدام نظام Linux 4.8.2 x86_64 Linux:

gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o

انتاج:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    $0x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       75 14                   jne    24 <main+0x24>
  10:       ba 01 00 00 00          mov    $0x1,%edx
  15:       be 00 00 00 00          mov    $0x0,%esi
                    16: R_X86_64_32 .rodata.str1.1
  1a:       bf 01 00 00 00          mov    $0x1,%edi
  1f:       e8 00 00 00 00          callq  24 <main+0x24>
                    20: R_X86_64_PC32       __printf_chk-0x4
  24:       bf 00 00 00 00          mov    $0x0,%edi
                    25: R_X86_64_32 .rodata.str1.1+0x4
  29:       e8 00 00 00 00          callq  2e <main+0x2e>
                    2a: R_X86_64_PC32       puts-0x4
  2e:       31 c0                   xor    %eax,%eax
  30:       48 83 c4 08             add    $0x8,%rsp
  34:       c3                      retq

لم يتغير ترتيب التعليمات في الذاكرة:لأول مرة printf وثم puts و ال retq يعود.

مع __builtin_expect

الآن استبدل if (i) مع:

if (__builtin_expect(i, 0))

ونحصل على:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    $0x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       74 11                   je     21 <main+0x21>
  10:       bf 00 00 00 00          mov    $0x0,%edi
                    11: R_X86_64_32 .rodata.str1.1+0x4
  15:       e8 00 00 00 00          callq  1a <main+0x1a>
                    16: R_X86_64_PC32       puts-0x4
  1a:       31 c0                   xor    %eax,%eax
  1c:       48 83 c4 08             add    $0x8,%rsp
  20:       c3                      retq
  21:       ba 01 00 00 00          mov    $0x1,%edx
  26:       be 00 00 00 00          mov    $0x0,%esi
                    27: R_X86_64_32 .rodata.str1.1
  2b:       bf 01 00 00 00          mov    $0x1,%edi
  30:       e8 00 00 00 00          callq  35 <main+0x35>
                    31: R_X86_64_PC32       __printf_chk-0x4
  35:       eb d9                   jmp    10 <main+0x10>

ال printf (تم تجميعها ل __printf_chk) تم نقله إلى نهاية الوظيفة، بعد ذلك puts والعودة لتحسين التنبؤ بالفرع كما ذكرت الإجابات الأخرى.

لذا فهو في الأساس نفس:

int i = !time(NULL);
if (i)
    goto printf;
puts:
puts("a");
return 0;
printf:
printf("%d\n", i);
goto puts;

لم يتم هذا التحسين مع -O0.

لكن حظًا سعيدًا في كتابة مثال يعمل بشكل أسرع __builtin_expect من دون، وحدات المعالجة المركزية (CPUs) ذكية حقًا في تلك الأيام.محاولاتي الساذجة هنا.

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

على سبيل المثال، في وحدة المعالجة المركزية PowerPC، قد يستغرق الفرع غير المُلمح 16 دورة، وواحدة مُلمحة بشكل صحيح 8 ودورة مُلمحة بشكل غير صحيح 24 دورة.في الحلقات الأعمق، يمكن للتلميحات الجيدة أن تحدث فرقًا هائلاً.

لا تمثل إمكانية النقل مشكلة حقًا - من المفترض أن يكون التعريف موجودًا في رأس كل منصة؛يمكنك ببساطة تحديد "محتمل" و"غير محتمل" إلى لا شيء للأنظمة الأساسية التي لا تدعم تلميحات الفروع الثابتة.

long __builtin_expect(long EXP, long C);

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

#define unlikely(expr) __builtin_expect(!!(expr), 0)
#define likely(expr) __builtin_expect(!!(expr), 1)

ويمكن بعد ذلك استخدام وحدات الماكرو هذه كما في

if (likely(a > 1))

مرجع: https://www.akkadia.org/drepper/cpumemory.pdf

(تعليق عام - الإجابات الأخرى تغطي التفاصيل)

لا يوجد سبب يجعلك تفقد إمكانية النقل باستخدامها.

لديك دائمًا خيار إنشاء ماكرو "مضمن" أو ماكرو بسيط ذو تأثير صفري يسمح لك بالتجميع على منصات أخرى باستخدام مترجمين آخرين.

لن تستفيد من التحسين إذا كنت تستخدم منصات أخرى.

حسب تعليق بواسطة كودي, ، هذا لا علاقة له بنظام Linux، ولكنه تلميح للمترجم.ما سيحدث سيعتمد على البنية وإصدار المترجم.

يتم إساءة استخدام هذه الميزة الخاصة في Linux إلى حد ما في برامج التشغيل.مثل com.osgx يشير في دلالات السمة الساخنة, ، أي hot أو cold يمكن للوظيفة التي يتم استدعاؤها في كتلة أن تشير تلقائيًا إلى أن الحالة محتملة أم لا.على سبيل المثال، dump_stack() انه محدد cold لذلك هذا زائدة عن الحاجة،

 if(unlikely(err)) {
     printk("Driver error found. %d\n", err);
     dump_stack();
 }

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

في العديد من إصدارات Linux، يمكنك العثور على complier.h في /usr/linux/، ويمكنك تضمينه للاستخدام ببساطة.ورأي آخر، غير محتمل() أكثر فائدة من المحتمل()، لأن

if ( likely( ... ) ) {
     doSomething();
}

يمكن تحسينه أيضًا في العديد من المترجمات.

وبالمناسبة، إذا كنت تريد مراقبة السلوك التفصيلي للكود، فيمكنك القيام بذلك ببساطة كما يلي:

gcc -c test.c objdump -d test.o> obj.s

ثم افتح obj.s، يمكنك العثور على الإجابة.

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

يحرر:لقد نسيت مكانًا واحدًا يمكنهم المساعدة فيه بالفعل.يمكن أن يسمح للمترجم بإعادة ترتيب الرسم البياني للتحكم في التدفق لتقليل عدد الفروع المأخوذة للمسار "المحتمل".يمكن أن يكون لهذا تحسن ملحوظ في الحلقات حيث تقوم بالتحقق من حالات الخروج المتعددة.

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

تعتمد كيفية إنشاء تعليمات الفرع على بنية المعالج.

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