c++ - لماذا تولد GCC رمزًا أسرع بنسبة 15 إلى 20٪ إذا قمت بتحسين الحجم بدلاً من السرعة؟




performance assembly (4)

أعتقد أنه يمكنك الحصول على النتيجة نفسها كما فعلت:

أمسكت التجميع ل- O2 ودمج كل الاختلافات في الجمعية ل- باستثناء باستثناء خطوط. p2align:

... باستخدام -O2 -falign-functions=1 -falign-jumps=1 -falign-loops=1 -falign-labels=1 . لقد قمت بتجميع كل شيء باستخدام هذه الخيارات ، التي كانت أسرع من -O2 كل مرة -O2 لقياسها ، لمدة 15 عامًا.

أيضا ، لسياق مختلف تماما (بما في ذلك مترجم مختلف) ، لاحظت أن الوضع مشابه : الخيار الذي من المفترض أن "يحسن حجم الكود بدلا من السرعة" يحسن لحجم الرمز والسرعة.

إذا كنت أعتقد بشكل صحيح ، هذه هي paddings لمحاذاة المكدس.

لا ، هذا ليس له علاقة بالمكدس ، NOPs التي يتم إنشاؤها بشكل افتراضي ، وأن الخيارات -falign - * = 1 لمنع لمحاذاة التعليمات البرمجية.

وفقاً لِمَ تعمل وظائف لوحة المفاتيح في دول مجلس التعاون الخليجي مع NOPs؟ يتم ذلك على أمل أن يتم تشغيل رمز أسرع ولكن يبدو أن هذا التحسين بنتائج عكسية في حالتي.

هل هو الحشو الذي هو الجاني في هذه الحالة؟ لماذا و كيف؟

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

لقد لاحظت لأول مرة في عام 2009 أن دول مجلس التعاون الخليجي (على الأقل في -O2 -O3 ) لديها ميل لتوليد رمز أسرع بشكل ملحوظ إذا قمت بتحسين حجم ( -Os ) بدلاً من السرعة ( -O2 أو -O3 ) ، ولقد تم أتساءل منذ ذلك الحين لماذا.

لقد تمكنت من إنشاء رمز (بدلاً من ذلك سخيف) يظهر هذا السلوك المدهش وهو صغير بما فيه الكفاية ليتم نشره هنا.

const int LOOP_BOUND = 200000000;

__attribute__((noinline))
static int add(const int& x, const int& y) {
    return x + y;
}

__attribute__((noinline))
static int work(int xval, int yval) {
    int sum(0);
    for (int i=0; i<LOOP_BOUND; ++i) {
        int x(xval+sum);
        int y(yval+sum);
        int z = add(x, y);
        sum += z;
    }
    return sum;
}

int main(int , char* argv[]) {
    int result = work(*argv[1], *argv[2]);
    return result;
}

إذا قمت بتجميعها باستخدام -O2 -O3 إلى 0.38 ثانية لتنفيذ هذا البرنامج ، و 0.44 ثانية إذا تم تجميعها مع -O2 أو -O3 . يتم الحصول على هذه الأوقات باستمرار وبدون أي ضوضاء (gcc 4.7.2 ، x 86_64 GNU / Linux ، Intel Core i5-3320M).

(تحديث: لقد قمت بنقل كافة التعليمات البرمجية التجميعية إلى GitHub : قاموا بنشر هذه الوظيفة المتضخمة ويبدو أنهم يضيفون قيمة قليلة جدًا للأسئلة حيث أن fno-align-* لها نفس التأثير.)

هنا هو التجمع الذي تم إنشاؤه مع -O2 و -O2 .

لسوء الحظ ، فإن فهمي للتجميع محدود للغاية ، لذلك ليس لدي أي فكرة عما إذا كان ما فعلته بعد ذلك صحيحًا: قمت -O2 التجميع لـ -O2 ودمجت جميع اختلافاته في التجميع لـ .p2align باستثناء خطوط .p2align ، النتيجة here . هذا الرمز لا يزال يعمل في 0.38s والفرق الوحيد هو الاشياء. .p2align .

إذا كنت أعتقد بشكل صحيح ، هذه هي paddings لمحاذاة المكدس. وفقاً لِمَ تعمل وظائف لوحة المفاتيح في دول مجلس التعاون الخليجي مع NOPs؟ يتم ذلك على أمل أن يتم تشغيل رمز أسرع ، ولكن يبدو أن هذا التحسين بنتائج عكسية في حالتي.

هل هو الحشو الذي هو الجاني في هذه الحالة؟ لماذا و كيف؟

الضجيج الذي يجعله إلى حد كبير يجعل توقيت التحسينات الصغرى مستحيلة.

كيف يمكنني التأكد من أن محاولات الحظ / الحظ غير المتعمدة هذه لا تتدخل عندما أقوم بإجراء تحسينات صغيرة (لا علاقة لها بمحاذاة الرص) على كود مصدر C أو C ++؟

تحديث:

بعد إجابة باسكال كوق ، قمت بتدبير بعض الشيء مع المحاذاة. عن طريق تمرير -O2 -fno-align-functions -fno-align-loops إلى gcc ، يتم .p2align جميع .p2align من التجميع ويتم تشغيل الملف التنفيذي القابل للتنفيذ في 0.38 ثانية. وفقًا لوثائق لجنة التحكيم :

-Os تمكن جميع -O2 التحسينات [ولكن] -Os تعطيل علامات التحسين التالية:

  -falign-functions  -falign-jumps  -falign-loops <br/>
  -falign-labels  -freorder-blocks  -freorder-blocks-and-partition <br/>
  -fprefetch-loop-arrays <br/>

لذا ، يبدو الأمر إلى حد كبير مثل مشكلة المحاذاة.

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

تحديث 2:

يمكننا أخذ -Os من الصورة. يتم الحصول على الأوقات التالية عن طريق تجميع

  • -O2 -fno-omit-frame-pointer 0.37s

  • -O2 -fno-align-functions -fno-align-loops 0.37s

  • -S -O2 ثم نقل مجموعة التجميع يدوياً add() بعد work() 0.37 ثانية

  • -O2 0.44s

يبدو لي أن مسافة add() من موقع الاتصال يهم الكثير. لقد جربت perf ، ولكن ناتج perf report كثيرا. ومع ذلك ، يمكنني فقط الحصول على نتيجة واحدة متسقة للخروج منه:

-O2 :

 602,312,864 stalled-cycles-frontend   #    0.00% frontend cycles idle
       3,318 cache-misses
 0.432703993 seconds time elapsed
 [...]
 81.23%  a.out  a.out              [.] work(int, int)
 18.50%  a.out  a.out              [.] add(int const&, int const&) [clone .isra.0]
 [...]
       ¦   __attribute__((noinline))
       ¦   static int add(const int& x, const int& y) {
       ¦       return x + y;
100.00 ¦     lea    (%rdi,%rsi,1),%eax
       ¦   }
       ¦   ? retq
[...]
       ¦            int z = add(x, y);
  1.93 ¦    ? callq  add(int const&, int const&) [clone .isra.0]
       ¦            sum += z;
 79.79 ¦      add    %eax,%ebx

من أجل fno-align-* :

 604,072,552 stalled-cycles-frontend   #    0.00% frontend cycles idle
       9,508 cache-misses
 0.375681928 seconds time elapsed
 [...]
 82.58%  a.out  a.out              [.] work(int, int)
 16.83%  a.out  a.out              [.] add(int const&, int const&) [clone .isra.0]
 [...]
       ¦   __attribute__((noinline))
       ¦   static int add(const int& x, const int& y) {
       ¦       return x + y;
 51.59 ¦     lea    (%rdi,%rsi,1),%eax
       ¦   }
[...]
       ¦    __attribute__((noinline))
       ¦    static int work(int xval, int yval) {
       ¦        int sum(0);
       ¦        for (int i=0; i<LOOP_BOUND; ++i) {
       ¦            int x(xval+sum);
  8.20 ¦      lea    0x0(%r13,%rbx,1),%edi
       ¦            int y(yval+sum);
       ¦            int z = add(x, y);
 35.34 ¦    ? callq  add(int const&, int const&) [clone .isra.0]
       ¦            sum += z;
 39.48 ¦      add    %eax,%ebx
       ¦    }

For -fno-omit-frame-pointer :

 404,625,639 stalled-cycles-frontend   #    0.00% frontend cycles idle
      10,514 cache-misses
 0.375445137 seconds time elapsed
 [...]
 75.35%  a.out  a.out              [.] add(int const&, int const&) [clone .isra.0]                                                                                     ¦
 24.46%  a.out  a.out              [.] work(int, int)
 [...]
       ¦   __attribute__((noinline))
       ¦   static int add(const int& x, const int& y) {
 18.67 ¦     push   %rbp
       ¦       return x + y;
 18.49 ¦     lea    (%rdi,%rsi,1),%eax
       ¦   const int LOOP_BOUND = 200000000;
       ¦
       ¦   __attribute__((noinline))
       ¦   static int add(const int& x, const int& y) {
       ¦     mov    %rsp,%rbp
       ¦       return x + y;
       ¦   }
 12.71 ¦     pop    %rbp
       ¦   ? retq
 [...]
       ¦            int z = add(x, y);
       ¦    ? callq  add(int const&, int const&) [clone .isra.0]
       ¦            sum += z;
 29.83 ¦      add    %eax,%ebx

يبدو أننا نتوقف عن الاتصال add() في الحالة البطيئة.

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

بالنسبة إلى الملف التنفيذي نفسه ، تُظهر stalled-cycles-frontend ارتباطًا طوليًا بوقت التنفيذ ؛ لم ألاحظ أي شيء آخر يرتبط بشكل واضح. (لا تعني المقارنة stalled-cycles-frontend التوقف stalled-cycles-frontend التنفيذية المختلفة أي معنى بالنسبة لي.)

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


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

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

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


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

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

أعتقد أنك تواجه زاوية مختلفة في نفس الملاحظة.

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


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

فيما يلي نتائج time ./test 0 0 على عدة معالجات (تم تسجيل وقت المستخدم):

Processor (System-on-Chip)             Compiler   Time (-O2)  Time (-Os)  Fastest
AMD Opteron 8350                       gcc-4.8.1    0.704s      0.896s      -O2
AMD FX-6300                            gcc-4.8.1    0.392s      0.340s      -Os
AMD E2-1800                            gcc-4.7.2    0.740s      0.832s      -O2
Intel Xeon E5405                       gcc-4.8.1    0.603s      0.804s      -O2
Intel Xeon E5-2603                     gcc-4.4.7    1.121s      1.122s       -
Intel Core i3-3217U                    gcc-4.6.4    0.709s      0.709s       -
Intel Core i3-3217U                    gcc-4.7.3    0.708s      0.822s      -O2
Intel Core i3-3217U                    gcc-4.8.1    0.708s      0.944s      -O2
Intel Core i7-4770K                    gcc-4.8.1    0.296s      0.288s      -Os
Intel Atom 330                         gcc-4.8.1    2.003s      2.007s      -O2
ARM 1176JZF-S (Broadcom BCM2835)       gcc-4.6.3    3.470s      3.480s      -O2
ARM Cortex-A8 (TI OMAP DM3730)         gcc-4.6.3    2.727s      2.727s       -
ARM Cortex-A9 (TI OMAP 4460)           gcc-4.6.3    1.648s      1.648s       -
ARM Cortex-A9 (Samsung Exynos 4412)    gcc-4.6.3    1.250s      1.250s       -
ARM Cortex-A15 (Samsung Exynos 5250)   gcc-4.7.2    0.700s      0.700s       -
Qualcomm Snapdragon APQ8060A           gcc-4.8       1.53s       1.52s      -Os

في بعض الحالات ، يمكنك التخفيف من تأثير التحسينات غير الملائمة من خلال -mtune=native -march=native ) بتحسين المعالج الخاص بك (باستخدام خيارات -mtune=native أو -march=native ):

Processor            Compiler   Time (-O2 -mtune=native) Time (-Os -mtune=native)
AMD FX-6300          gcc-4.8.1         0.340s                   0.340s
AMD E2-1800          gcc-4.7.2         0.740s                   0.832s
Intel Xeon E5405     gcc-4.8.1         0.603s                   0.803s
Intel Core i7-4770K  gcc-4.8.1         0.296s                   0.288s

تحديث: على Ivy Bridge-based Core i3 ثلاثة إصدارات من دول gcc التعاون gcc ( 4.6.4 ، 4.7.3 ، و 4.8.1 ) تنتج الثنائيات مع أداء مختلف بشكل كبير ، ولكن رمز التجميع لديه اختلافات دقيقة فقط. حتى الآن ، ليس لدي أي تفسير لهذه الحقيقة.

تجميع من gcc-4.6.4 -Os -Os (ينفذ في 0.709 ثانية):

00000000004004d2 <_ZL3addRKiS0_.isra.0>:
  4004d2:       8d 04 37                lea    eax,[rdi+rsi*1]
  4004d5:       c3                      ret

00000000004004d6 <_ZL4workii>:
  4004d6:       41 55                   push   r13
  4004d8:       41 89 fd                mov    r13d,edi
  4004db:       41 54                   push   r12
  4004dd:       41 89 f4                mov    r12d,esi
  4004e0:       55                      push   rbp
  4004e1:       bd 00 c2 eb 0b          mov    ebp,0xbebc200
  4004e6:       53                      push   rbx
  4004e7:       31 db                   xor    ebx,ebx
  4004e9:       41 8d 34 1c             lea    esi,[r12+rbx*1]
  4004ed:       41 8d 7c 1d 00          lea    edi,[r13+rbx*1+0x0]
  4004f2:       e8 db ff ff ff          call   4004d2 <_ZL3addRKiS0_.isra.0>
  4004f7:       01 c3                   add    ebx,eax
  4004f9:       ff cd                   dec    ebp
  4004fb:       75 ec                   jne    4004e9 <_ZL4workii+0x13>
  4004fd:       89 d8                   mov    eax,ebx
  4004ff:       5b                      pop    rbx
  400500:       5d                      pop    rbp
  400501:       41 5c                   pop    r12
  400503:       41 5d                   pop    r13
  400505:       c3                      ret

تجميع من gcc-4.7.3 -Os 4.7.3 -Os (ينفذ في 0.822 ثانية):

00000000004004fa <_ZL3addRKiS0_.isra.0>:
  4004fa:       8d 04 37                lea    eax,[rdi+rsi*1]
  4004fd:       c3                      ret

00000000004004fe <_ZL4workii>:
  4004fe:       41 55                   push   r13
  400500:       41 89 f5                mov    r13d,esi
  400503:       41 54                   push   r12
  400505:       41 89 fc                mov    r12d,edi
  400508:       55                      push   rbp
  400509:       bd 00 c2 eb 0b          mov    ebp,0xbebc200
  40050e:       53                      push   rbx
  40050f:       31 db                   xor    ebx,ebx
  400511:       41 8d 74 1d 00          lea    esi,[r13+rbx*1+0x0]
  400516:       41 8d 3c 1c             lea    edi,[r12+rbx*1]
  40051a:       e8 db ff ff ff          call   4004fa <_ZL3addRKiS0_.isra.0>
  40051f:       01 c3                   add    ebx,eax
  400521:       ff cd                   dec    ebp
  400523:       75 ec                   jne    400511 <_ZL4workii+0x13>
  400525:       89 d8                   mov    eax,ebx
  400527:       5b                      pop    rbx
  400528:       5d                      pop    rbp
  400529:       41 5c                   pop    r12
  40052b:       41 5d                   pop    r13
  40052d:       c3                      ret

تجميع من gcc-4.8.1 -Os (ينفذ في 0.994 ثانية):

00000000004004fd <_ZL3addRKiS0_.isra.0>:
  4004fd:       8d 04 37                lea    eax,[rdi+rsi*1]
  400500:       c3                      ret

0000000000400501 <_ZL4workii>:
  400501:       41 55                   push   r13
  400503:       41 89 f5                mov    r13d,esi
  400506:       41 54                   push   r12
  400508:       41 89 fc                mov    r12d,edi
  40050b:       55                      push   rbp
  40050c:       bd 00 c2 eb 0b          mov    ebp,0xbebc200
  400511:       53                      push   rbx
  400512:       31 db                   xor    ebx,ebx
  400514:       41 8d 74 1d 00          lea    esi,[r13+rbx*1+0x0]
  400519:       41 8d 3c 1c             lea    edi,[r12+rbx*1]
  40051d:       e8 db ff ff ff          call   4004fd <_ZL3addRKiS0_.isra.0>
  400522:       01 c3                   add    ebx,eax
  400524:       ff cd                   dec    ebp
  400526:       75 ec                   jne    400514 <_ZL4workii+0x13>
  400528:       89 d8                   mov    eax,ebx
  40052a:       5b                      pop    rbx
  40052b:       5d                      pop    rbp
  40052c:       41 5c                   pop    r12
  40052e:       41 5d                   pop    r13
  400530:       c3                      ret




assembly