it-swarm.dev

لماذا تقديم تعليمات MOV عديمة الفائدة تسريع حلقة ضيقة في التجمع x86_64؟

الخلفية:

أثناء تحسين بعض Pascal code باستخدام لغة التجميع المدمجة ، لاحظت تعليمة MOV غير الضرورية ، وأزلتها.

لدهشتي ، تسببت إزالة التعليمات غير الضرورية في جعل برنامجي إبطاء.

لقد وجدت أن إضافة تعليمات MOV تعسفية وعديمة الفائدة زادت من الأداء أكثر من ذلك.

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

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

البيانات:

يقوم إصدار من التعليمات البرمجية الخاصة بي بترجمة مشروطة ثلاث عمليات غير مهمة في منتصف حلقة تعمل 2**20==1048576 مرة. (البرنامج المحيط يحسب فقط SHA-256 التجزئة).

النتائج على الجهاز القديم (Intel (R) Core (TM) 2 CPU 6400 @ 2.13 GHz):

avg time (ms) with -dJUNKOPS: 1822.84 ms
avg time (ms) without:        1836.44 ms

تم تشغيل البرامج 25 مرة في حلقة ، مع تغيير ترتيب التشغيل بشكل عشوائي في كل مرة.

مقتطفات:

{$asmmode intel}
procedure example_junkop_in_sha256;
  var s1, t2 : uint32;
  begin
    // Here are parts of the SHA-256 algorithm, in Pascal:
    // s0 {r10d} := ror(a, 2) xor ror(a, 13) xor ror(a, 22)
    // s1 {r11d} := ror(e, 6) xor ror(e, 11) xor ror(e, 25)
    // Here is how I translated them (side by side to show symmetry):
  asm
    MOV r8d, a                 ; MOV r9d, e
    ROR r8d, 2                 ; ROR r9d, 6
    MOV r10d, r8d              ; MOV r11d, r9d
    ROR r8d, 11    {13 total}  ; ROR r9d, 5     {11 total}
    XOR r10d, r8d              ; XOR r11d, r9d
    ROR r8d, 9     {22 total}  ; ROR r9d, 14    {25 total}
    XOR r10d, r8d              ; XOR r11d, r9d

    // Here is the extraneous operation that I removed, causing a speedup
    // s1 is the uint32 variable declared at the start of the Pascal code.
    //
    // I had cleaned up the code, so I no longer needed this variable, and 
    // could just leave the value sitting in the r11d register until I needed
    // it again later.
    //
    // Since copying to RAM seemed like a waste, I removed the instruction, 
    // only to discover that the code ran slower without it.
    {$IFDEF JUNKOPS}
    MOV s1,  r11d
    {$ENDIF}

    // The next part of the code just moves on to another part of SHA-256,
    // maj { r12d } := (a and b) xor (a and c) xor (b and c)
    mov r8d,  a
    mov r9d,  b
    mov r13d, r9d // Set aside a copy of b
    and r9d,  r8d

    mov r12d, c
    and r8d, r12d  { a and c }
    xor r9d, r8d

    and r12d, r13d { c and b }
    xor r12d, r9d

    // Copying the calculated value to the same s1 variable is another speedup.
    // As far as I can tell, it doesn't actually matter what register is copied,
    // but moving this line up or down makes a huge difference.
    {$IFDEF JUNKOPS}
    MOV s1,  r9d // after mov r12d, c
    {$ENDIF}

    // And here is where the two calculated values above are actually used:
    // T2 {r12d} := S0 {r10d} + Maj {r12d};
    ADD r12d, r10d
    MOV T2, r12d

  end
end;

جربها بنفسك:

الرمز متاح عبر الإنترنت في GitHub إذا كنت ترغب في تجربته بنفسك.

أسئلتي:

  • لماذا من غير المفيد نسخ محتويات السجل إلى RAM أي وقت مضى زيادة الأداء؟
  • لماذا توفر نفس التعليمات عديمة الفائدة تسريعًا على بعض الخطوط وتباطؤًا في خطوط أخرى؟
  • هل هذا السلوك شيء يمكن استغلاله بشكل متوقع من قبل المترجم؟
215
tangentstorm

السبب الأكثر ترجيحًا لتحسين السرعة هو:

  • يؤدي إدخال MOV إلى نقل التعليمات التالية إلى عناوين ذاكرة مختلفة
  • كان واحدا من تلك التعليمات التي تم نقلها فرع مشروط مهم
  • تم التنبؤ بهذا الفرع بشكل غير صحيح بسبب الاسم المستعار في جدول التنبؤ بالفرع
  • تحريك الفرع يزيل الاسم المستعار ويسمح للتنبؤ بالفرع بشكل صحيح

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

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

إذا كنت تريد أن تفهم كيف تؤثر التوقعات الخاطئة للفرع على الأداء ، فقم بإلقاء نظرة على هذه الإجابة الممتازة: { https://stackoverflow.com/a/11227902/1001643

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

139
Raymond Hettinger

قد ترغب في قراءة http://research.google.com/pubs/pub37077.html

TL ؛ DR: يمكن أن يؤدي إدخال تعليمات nop عشوائياً في البرامج إلى زيادة الأداء بسهولة بنسبة 5٪ أو أكثر ، ولا ، لا يمكن لبرنامج التحويل البرمجي استغلال ذلك بسهولة. عادة ما يكون ذلك عبارة عن مزيج من سلوك الفروع والتنبؤ بالذاكرة المؤقتة ، ولكن يمكن أن يكون كذلك على سبيل المثال كشك محطة حجز (حتى في حالة عدم وجود سلاسل تبعية مقطوعة أو اشتراكات موارد زائدة واضحة على الإطلاق).

78
Jonas Maebe

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

وحدات المعالجة المركزية الحديثة هي RISC / CISC / الهجينة التي تترجم تعليمات CISC إلى x86 إلى تعليمات داخلية تكون أكثر RISC في السلوك. بالإضافة إلى ذلك ، يوجد محللو تنفيذ خارج الترتيب ومتنبئون للفروع و "دمج العمليات الصغرى" من Intel يحاولون تجميع التعليمات في مجموعات أكبر من العمل المتزامن (نوع من مثل VLIW / Itanium عملاق). حتى أن هناك حدود لذاكرة التخزين المؤقت يمكن أن تجعل الشفرة تعمل بشكل أسرع لمعرفة الله - لماذا إذا كانت أكبر (ربما تحكمها ذاكرة التخزين المؤقت بفتحة أكبر ، أو تبقيه أطول).

كان لدى CISC دائمًا طبقة ترجمة من التجميع إلى الرمز الصغير ، ولكن النقطة المهمة هي أنه مع وحدات المعالجة المركزية الحديثة تصبح الأمور أكثر تعقيدًا بكثير. مع كل العقارات الإضافية للترانزستور في مصانع تصنيع أشباه الموصلات الحديثة ، يمكن لوحدات المعالجة المركزية (CPUs) أن تطبق على الأرجح العديد من مناهج التحسين بشكل متوازٍ ثم حدد الطريقة في النهاية التي توفر أفضل تسريع. قد تكون الإرشادات الإضافية متحيزة في وحدة المعالجة المركزية لاستخدام مسار تحسين واحد أفضل من الآخرين.

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

13
cowarldlydragon