เหตุใด GCC จึงสร้างรหัสเร็วขึ้น 15-20% หากฉันปรับขนาดให้เหมาะสมแทนความเร็ว


445

ฉันสังเกตเห็นครั้งแรกในปี 2009 ว่า GCC (อย่างน้อยในโครงการของฉันและในเครื่องของฉัน) มีแนวโน้มที่จะสร้างรหัสที่เร็วขึ้นอย่างเห็นได้ชัดถ้าฉันปรับขนาด ( -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;
}

ถ้าผมรวบรวมไว้ด้วย-Osก็จะใช้เวลา 0.38 วินาทีในการรันโปรแกรมนี้และ 0.44 s ถ้ามันจะรวบรวมกับหรือ-O2 -O3เวลาเหล่านี้ได้รับอย่างสม่ำเสมอและไม่มีเสียงรบกวน (gcc 4.7.2, x86_64 GNU / Linux, Intel Core i5-3320M)

(อัปเดต: ฉันย้ายรหัสแอสเซมบลีทั้งหมดไปที่GitHub : พวกเขาทำให้โพสต์ป่องและเห็นได้ชัดว่าเพิ่มมูลค่าน้อยมากสำหรับคำถามเนื่องจากfno-align-*ค่าสถานะมีผลเหมือนกัน)

ที่นี่เป็นที่ที่สร้างขึ้นประกอบกับและ-Os-O2

แต่น่าเสียดายที่ความเข้าใจของฉันของการชุมนุมถูก จำกัด มากดังนั้นฉันมีความคิดว่าสิ่งที่ผมทำต่อไปได้ถูกต้องไม่มี: ฉันคว้าประกอบ-O2และผสานความแตกต่างทั้งหมดไปสู่การชุมนุมสำหรับการ-Os ยกเว้น.p2alignเส้นส่งผลให้ที่นี่ รหัสนี้ยังคงทำงานใน 0.38s และสิ่งที่แตกต่างคือ .p2align สิ่งที่

หากฉันเดาถูกต้องสิ่งเหล่านี้เป็นช่องว่างสำหรับการจัดเรียงสแต็ค ตามที่เหตุใด GCC pad จึงทำงานกับ NOP มันทำด้วยหวังว่ารหัสจะทำงานได้เร็วขึ้น แต่เห็นได้ชัดว่าการเพิ่มประสิทธิภาพนี้ backfired ในกรณีของฉัน

มันเป็นช่องว่างภายในที่เป็นผู้กระทำผิดในกรณีนี้หรือไม่? ทำไมและอย่างไร

เสียงรบกวนที่ทำให้สวยทำให้การปรับตั้งเวลาแบบไมโครเป็นไปไม่ได้

ฉันจะแน่ใจได้อย่างไรว่าการจัดเรียง Lucky / Unlucky โดยบังเอิญไม่รบกวนเมื่อฉันทำการปรับให้เหมาะสมแบบไมโคร (ไม่เกี่ยวข้องกับการจัดเรียงสแต็ค) ในซอร์สโค้ด C หรือ C ++


UPDATE:

การทำตามคำตอบของ Pascal Cuoqฉันได้นิดหน่อยกับการจัดแนวเล็กน้อย เมื่อผ่าน-O2 -fno-align-functions -fno-align-loopsไปยัง gcc ทั้งหมด.p2alignจะหายไปจากชุดประกอบและชุดประมวลผลที่สร้างขึ้นจะทำงานใน 0.38 วินาที ตามเอกสาร gcc :

-Os เปิดใช้งานการปรับให้เหมาะสม -O2 ทั้งหมด [แต่] -Os ปิดใช้งานแฟล็กการปรับให้เหมาะสมต่อไปนี้:

  -falign-functions  -falign-jumps  -falign-loops
  -falign-labels  -freorder-blocks  -freorder-blocks-and-partition
  -fprefetch-loop-arrays

ดังนั้นดูเหมือนว่าปัญหาการจัดตำแหน่งผิดพลาด

ฉันยังคงสงสัยเกี่ยวกับการ-march=nativeตามข้อเสนอแนะในคำตอบของ Marat Dukhan ฉันไม่เชื่อว่ามันไม่ได้เป็นเพียงแค่การแทรกแซงปัญหาการจัดตำแหน่ง (ผิด) นี้; มันไม่มีผลกับเครื่องของฉันเลย (ถึงกระนั้นฉัน upvoted คำตอบของเขา)


อัปเดต 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 statและ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
       ¦    }

สำหรับ-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()ในกรณีที่ช้า

ฉันได้ตรวจสอบทุกสิ่งที่perf -eสามารถพ่นออกมาในเครื่องของฉัน; ไม่ใช่แค่สถิติที่ให้ไว้ข้างต้น

สำหรับปฏิบัติการที่เหมือนกันการstalled-cycles-frontendแสดงความสัมพันธ์เชิงเส้นกับเวลาดำเนินการ ฉันไม่ได้สังเกตเห็นสิ่งอื่นใดที่จะมีความสัมพันธ์อย่างชัดเจน (การเปรียบเทียบstalled-cycles-frontendสำหรับไฟล์เรียกทำงานที่แตกต่างกันนั้นไม่สมเหตุสมผลสำหรับฉัน)

ฉันรวมแคชที่หายไปเนื่องจากเป็นความคิดเห็นแรก ฉันตรวจสอบการพลาดแคชทั้งหมดที่สามารถวัดได้บนเครื่องของฉันด้วยperfไม่ใช่เฉพาะที่ระบุไว้ด้านบน แคชคิดถึงมากและมีเสียงดังมากและไม่มีความสัมพันธ์กับเวลาดำเนินการ


36
คนตาบอดเดา: นี่อาจจะเป็นแคชพลาด

@ H2CO3 นั่นเป็นความคิดแรกของฉันเช่นกัน แต่ก็ไม่ได้รับการสนับสนุนเพียงพอที่จะโพสต์ความคิดเห็นโดยไม่ต้องอ่านและทำความเข้าใจคำถามของ OP ในเชิงลึก
πάνταῥεῖ

2
@ g-makulik นั่นคือเหตุผลที่ฉันเตือนว่ามันเป็น "blind guess" ;-) "TL; DR" ถูกสงวนไว้สำหรับคำถามที่ไม่ดี : P

3
เป็นเพียงจุดข้อมูลที่น่าสนใจ: ฉันพบว่า -O3 หรือ -Ofast เร็วประมาณ 1.5 เท่าเมื่อ -Os เมื่อฉันคอมไพล์ด้วย clang บน OS X (ฉันไม่ได้ลองทำซ้ำกับ gcc)
Rob Napier

2
มันเป็นรหัสเดียวกัน ดูที่อยู่ของ. L3 เป้าหมายสาขาที่ไม่ตรงแนวที่มีราคาแพง
Hans Passant

คำตอบ:


505

โดยคอมไพเลอร์เริ่มต้นปรับให้เหมาะสมสำหรับโปรเซสเซอร์ "เฉลี่ย" ตั้งแต่การประมวลผลที่แตกต่างกันโปรดปรานลำดับการเรียนการสอนที่แตกต่างกัน, การเพิ่มประสิทธิภาพคอมไพเลอร์เปิดใช้งานโดย-O2ผลประโยชน์ที่อาจประมวลผลค่าเฉลี่ย แต่ประสิทธิภาพการทำงานลดลงโดยเฉพาะอย่างยิ่งในการประมวลผลของคุณ (และเช่นเดียวกับ-Os) หากคุณลองเช่นเดียวกันในการประมวลผลที่แตกต่างกันคุณจะพบว่าในบางส่วนของพวกเขาได้รับประโยชน์จาก-O2ขณะที่อื่น ๆ จะดีขึ้นไป-Osเพิ่มประสิทธิภาพ

นี่คือผลลัพธ์สำหรับ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

ในบางกรณีคุณสามารถบรรเทาผลกระทบของการปรับให้เสียเปรียบโดยขอgccให้ปรับแต่งโปรเซสเซอร์ของคุณ (โดยใช้ตัวเลือก-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( 4.6.4, 4.7.3และ4.8.1 ) ไบนารีการผลิตที่มีประสิทธิภาพการทำงานที่แตกต่างกันอย่างมีนัยสำคัญ แต่รหัสการชุมนุมมีเพียงรูปแบบที่ลึกซึ้ง จนถึงตอนนี้ฉันยังไม่มีคำอธิบายของข้อเท็จจริงนี้

การชุมนุมจากgcc-4.6.4 -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(ดำเนินการใน 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

186
เพื่อให้ชัดเจน: คุณได้ไปจริง ๆ และวัดประสิทธิภาพของรหัสของ OP ใน 12 แพลตฟอร์มที่แตกต่างกันหรือไม่? (+1 สำหรับคนที่คิดว่าคุณจะทำอย่างนั้น)
Anatolyg

194
@anatolyg ใช่ฉันทำแล้ว! (และจะเพิ่มเร็ว ๆ นี้)
Marat Dukhan

43
จริง อีก +1 สำหรับไม่เพียง แต่ทฤษฎีเกี่ยวกับซีพียูที่แตกต่างกัน แต่ที่จริงพิสูจน์มัน ไม่ใช่สิ่งที่คุณเห็นในทุกคำตอบเกี่ยวกับความเร็ว การทดสอบเหล่านี้ทำงานด้วยระบบปฏิบัติการเดียวกันหรือไม่ (เนื่องจากอาจเป็นไปได้สิ่งนี้ทำให้ผลเสีย ... )
usr2564301

7
@Ali บน AMD-FX 6300 -O2 -fno-align-functions -fno-align-loopsลดเวลาลงไป0.340sดังนั้นสามารถอธิบายได้ด้วยการจัดเรียง อย่างไรก็ตามการจัดตำแหน่งที่เหมาะสมนั้นขึ้นอยู่กับหน่วยประมวลผลกลาง: โปรเซสเซอร์บางรุ่นต้องการลูปและฟังก์ชันที่จัดแนว
Marat Dukhan

13
@Jongware ฉันไม่เห็นว่าระบบปฏิบัติการจะมีอิทธิพลต่อผลลัพธ์อย่างไร ลูปไม่เคยโทรออกระบบ
อาลี

186

เพื่อนร่วมงานของฉันช่วยฉันหาคำตอบที่เป็นไปได้สำหรับคำถามของฉัน เขาสังเกตเห็นความสำคัญของขอบเขต 256 ไบต์ เขาไม่ได้ลงทะเบียนที่นี่และสนับสนุนให้ฉันโพสต์คำตอบด้วยตัวเอง (และรับชื่อเสียงทั้งหมด)


คำตอบสั้น ๆ :

มันเป็นช่องว่างภายในที่เป็นผู้กระทำผิดในกรณีนี้หรือไม่? ทำไมและอย่างไร

ทุกอย่างจะไปถึงตำแหน่ง การจัดตำแหน่งอาจมีผลกระทบอย่างมีนัยสำคัญต่อประสิทธิภาพนั่นคือสาเหตุที่เรามี-falign-*ธงในครั้งแรก

ผมได้ส่ง(ที่ปลอม?) รายงานข้อผิดพลาดให้กับนักพัฒนาจีซี ปรากฎว่าพฤติกรรมเริ่มต้นคือ"เราจัดเรียงลูปถึง 8 ไบต์ตามค่าเริ่มต้น แต่พยายามปรับให้เป็น 16 ไบต์หากเราไม่ต้องการเติมมากกว่า 10 ไบต์" เห็นได้ชัดว่าค่าเริ่มต้นนี้ไม่ใช่ตัวเลือกที่ดีที่สุดในกรณีนี้และในเครื่องของฉัน เสียงดังกังวาน 3.4 (งวง) กับ-O3การจัดตำแหน่งที่เหมาะสมและรหัสที่สร้างไม่ได้แสดงพฤติกรรมแปลก ๆ นี้

แน่นอนว่าหากการจัดแนวที่ไม่เหมาะสมนั้นจะทำให้สิ่งเลวร้ายยิ่งขึ้น การจัดตำแหน่งที่ไม่จำเป็น / ไม่ดีเพียงแค่กินไบต์โดยไม่มีเหตุผลและอาจเพิ่มแคชคิดถึง ฯลฯ

เสียงรบกวนที่ทำให้สวยทำให้การปรับตั้งเวลาแบบไมโครเป็นไปไม่ได้

ฉันจะแน่ใจได้อย่างไรว่าการจัดเรียง Lucky / Unlucky โดยบังเอิญไม่รบกวนเมื่อฉันทำการปรับให้เหมาะสมแบบไมโคร (ไม่เกี่ยวข้องกับการจัดเรียงสแต็ค) บนซอร์สโค้ด C หรือ C ++

เพียงแค่บอก gcc ให้ทำการจัดตำแหน่งที่ถูกต้อง:

g++ -O2 -falign-functions=16 -falign-loops=16


คำตอบยาว:

รหัสจะทำงานช้าลงหาก:

  • มีการXXตัดขอบเขตไบต์add()ที่ตรงกลาง (XXขึ้นอยู่กับเครื่อง)

  • หากการเรียกร้องให้add()มีการกระโดดข้ามXXเขตแดนไบต์และเป้าหมายนั้นไม่ได้ถูกจัดตำแหน่ง

  • ถ้า add()ไม่ได้รับการจัดตำแหน่ง

  • ถ้าลูปไม่ได้จัดตำแหน่ง

เป็นครั้งแรกที่ 2 สามารถมองเห็นได้อย่างสวยงามกับรหัสและผลที่Marat Dukhan โพสต์กรุณา ในกรณีนี้gcc-4.8.1 -Os(ดำเนินการใน 0.994 วินาที):

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

ขอบเขต 256 ไบต์ตัดadd()ตรงกลางและไม่ใช่add()จัดเรียงเป็นวง แปลกใจแปลกใจนี่เป็นกรณีที่ช้าที่สุด!

ในกรณีgcc-4.7.3 -Os(ดำเนินการใน 0.822 วินาที) ขอบเขต 256 ไบต์จะตัดเฉพาะส่วนที่เป็นหวัด (แต่ไม่ใช่ทั้งลูปและไม่add()ถูกตัด):

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

[...]

  40051a:       e8 db ff ff ff          call   4004fa <_ZL3addRKiS0_.isra.0>

ไม่มีการจัดตำแหน่งและการโทรไปที่ add()ให้ข้ามขอบเขต 256 ไบต์ รหัสนี้ช้าที่สุดที่สอง

ในกรณีgcc-4.6.4 -Os(เรียกใช้งานใน 0.709 วินาที) แม้ว่าจะไม่มีการปรับแนวใดก็ตามการเรียกไปadd()ยังไม่จำเป็นต้องกระโดดข้ามขอบเขต 256 ไบต์และเป้าหมายอยู่ห่างออกไป 32 ไบต์:

  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>

นี่เป็นวิธีที่เร็วที่สุดของทั้งสาม ทำไมขอบเขต 256 ไบต์เป็นพิเศษบนเครื่องของเขาฉันจะปล่อยให้เขาคิดออก ฉันไม่มีโปรเซสเซอร์ดังกล่าว

ตอนนี้ในเครื่องของฉันฉันไม่ได้รับผลกระทบ 256 ไบต์นี้ มีเพียงฟังก์ชั่นและการจัดแนวลูปที่จะเข้ามาในเครื่องของฉัน ถ้าฉันผ่านg++ -O2 -falign-functions=16 -falign-loops=16แล้วทุกอย่างกลับสู่ปกติ: ฉันจะได้รับคดีที่เร็วที่สุดและเวลาก็ไม่ไวต่อ-fno-omit-frame-pointerธงอีกต่อไป ฉันสามารถผ่านg++ -O2 -falign-functions=32 -falign-loops=32หรือทวีคูณใด ๆ ของ 16 รหัสไม่ไวต่อสิ่งนั้น

ฉันสังเกตเห็นครั้งแรกในปี 2009 ว่า gcc (อย่างน้อยในโครงการของฉันและบนเครื่องของฉัน) มีแนวโน้มที่จะสร้างรหัสที่เร็วขึ้นอย่างเห็นได้ชัดถ้าฉันปรับขนาด (-Os) แทนความเร็ว (-O2 หรือ -O3) และสงสัยว่า ตั้งแต่นั้นเป็นต้นมา

คำอธิบายที่น่าจะเป็นคือฉันมีฮอตสปอตซึ่งมีความอ่อนไหวต่อการจัดตำแหน่งเช่นเดียวกับตัวอย่างในตัวอย่างนี้ โดยการไปยุ่งกับธง (ผ่าน-Osแทน-O2) ฮอตสปอตเหล่านั้นได้รับการจัดเรียงในลักษณะที่โชคดีโดยบังเอิญและรหัสก็กลายเป็นเร็วขึ้นมันไม่เกี่ยวกับการปรับขนาดให้เหมาะสม: สิ่งเหล่านี้เกิดจากอุบัติเหตุที่ฮอตสปอตได้รับการจัดตำแหน่งที่ดีขึ้น จากนี้ไปฉันจะตรวจสอบผลกระทบของการจัดแนวในโครงการของฉัน

โอ้และอีกอย่างหนึ่ง ฮอตสปอตดังกล่าวจะเกิดขึ้นเช่นเดียวกับที่แสดงในตัวอย่างได้อย่างไร การอินไลน์ของฟังก์ชั่นเล็ก ๆ เช่นนี้add()ล้มเหลวได้อย่างไร?

พิจารณาสิ่งนี้:

// add.cpp
int add(const int& x, const int& y) {
    return x + y;
}

และในไฟล์แยกต่างหาก:

// main.cpp
int add(const int& x, const int& y);

const int LOOP_BOUND = 200000000;

__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;
}

g++ -O2 add.cpp main.cppและรวบรวมเป็น:

      gcc จะไม่อินไลน์add()!

นั่นคือทั้งหมดมันเป็นเรื่องง่ายที่จะสร้างฮอตสปอตโดยไม่ตั้งใจเช่นเดียวกับใน OP แน่นอนว่ามันเป็นความผิดของฉัน: gcc เป็นคอมไพเลอร์ที่ยอดเยี่ยม หากรวบรวมข้างต้นเป็น: g++ -O2 -flto add.cpp main.cppนั่นคือถ้าฉันทำการเพิ่มประสิทธิภาพเวลาลิงค์รหัสทำงานใน 0.19s!

(Inlining ถูกปิดใช้งานในเทียม OP ดังนั้นรหัสใน OP ช้าลง 2x)


19
ว้าว ... สิ่งนี้เหนือกว่าสิ่งที่ฉันมักทำเพื่อหลีกเลี่ยงความผิดปกติในการเปรียบเทียบ
Mysticial

@Ali ฉันเดาว่าเหมาะสมแล้วตั้งแต่คอมไพเลอร์จะแทรกสิ่งที่ไม่เห็นได้อย่างไร นั่นอาจเป็นเหตุผลที่เราใช้inline+ นิยามฟังก์ชั่นในส่วนหัว ไม่แน่ใจว่า lto ที่เป็นผู้ใหญ่จะอยู่ใน gcc ได้อย่างไร ประสบการณ์ของฉันกับมันอย่างน้อยใน mingw คือการตีหรือพลาด
greatwolf

7
ฉันคิดว่ามันเป็น Communications of ACM ที่มีบทความเมื่อไม่กี่ปีก่อนเกี่ยวกับการใช้งานแอพพลิเคชั่นที่ค่อนข้างใหญ่ (perl, Spice, ฯลฯ ) ในขณะที่ขยับอิมเมจไบนารี่ทั้งภาพทีละไบต์โดยใช้สภาพแวดล้อม Linux ขนาดแตกต่างกัน ฉันจำความแปรปรวนได้ 15% สรุปของพวกเขาคือผลการวัดประสิทธิภาพหลายอย่างนั้นไร้ประโยชน์เพราะตัวแปรภายนอกของการจัดตำแหน่งนั้นไม่ได้ถูกนำมาพิจารณา
ยีน

1
up'd -fltoโดยเฉพาะอย่างยิ่งสำหรับ มันค่อนข้างเป็นการปฏิวัติถ้าคุณไม่เคยใช้มันมาก่อนพูดจากประสบการณ์ :)
underscore_d

2
นี่คือวิดีโอที่ยอดเยี่ยมที่พูดถึงการจัดตำแหน่งที่สามารถส่งผลกระทบต่อประสิทธิภาพและวิธีการทำโปรไฟล์: youtube.com/watch?time_continue=1&v=r-TLSBdHe1A
Zhro

73

ฉันเพิ่มโพสต์ยอมรับนี้เพื่อชี้ให้เห็นว่าผลกระทบของการจัดตำแหน่งที่มีต่อประสิทธิภาพโดยรวมของโปรแกรม - รวมถึงการใหญ่ - ได้รับการศึกษา ตัวอย่างเช่นบทความนี้ (และฉันเชื่อว่ารุ่นนี้ยังปรากฏใน CACM) แสดงให้เห็นว่าลำดับการเชื่อมโยงและการเปลี่ยนแปลงขนาดสภาพแวดล้อมระบบปฏิบัติการเพียงอย่างเดียวเพียงพอที่จะเปลี่ยนประสิทธิภาพอย่างมีนัยสำคัญ พวกเขาคุณลักษณะนี้เพื่อการจัดตำแหน่งของ "ลูปร้อน"

บทความนี้มีชื่อว่า "การผลิตข้อมูลที่ผิดโดยไม่ทำอะไรผิดอย่างเห็นได้ชัด!" กล่าวว่าการตั้งค่าการทดลองโดยไม่ตั้งใจเนื่องจากความแตกต่างที่ควบคุมไม่ได้เกือบในสภาพแวดล้อมการทำงานของโปรแกรมอาจทำให้ผลลัพธ์การวัดประสิทธิภาพหลาย ๆ อันไม่มีความหมาย

ฉันคิดว่าคุณกำลังเผชิญหน้ากับมุมที่แตกต่างในการสังเกตเดียวกัน

สำหรับโค้ดที่มีประสิทธิภาพที่สำคัญนี่เป็นข้อโต้แย้งที่ดีสำหรับระบบที่ประเมินสภาพแวดล้อมในการติดตั้งหรือรันไทม์และเลือกท้องถิ่นที่ดีที่สุดในเวอร์ชันรูทีนคีย์ที่ปรับให้เหมาะสมที่สุด


33

ฉันคิดว่าคุณสามารถได้รับผลลัพธ์เช่นเดียวกับที่คุณทำ:

ฉันคว้าชุดประกอบสำหรับ -O2 และรวมความแตกต่างทั้งหมดไว้ในชุดประกอบสำหรับ -O ยกเว้นบรรทัด. p2align:

... -O2 -falign-functions=1 -falign-jumps=1 -falign-loops=1 -falign-labels=1โดยใช้ ฉันรวบรวมทุกอย่างด้วยตัวเลือกเหล่านี้ซึ่งเร็วกว่าธรรมดา-O2ทุกครั้งที่ฉันใส่ใจในการวัดเป็นเวลา 15 ปี

นอกจากนี้สำหรับบริบทที่แตกต่างอย่างสิ้นเชิง (รวมถึงคอมไพเลอร์ที่แตกต่างกัน) ฉันสังเกตเห็นว่าสถานการณ์คล้ายกัน : ตัวเลือกที่ควรจะ“ ปรับขนาดรหัสแทนความเร็ว” ปรับให้เหมาะสมสำหรับขนาดและความเร็วของรหัส

หากฉันเดาถูกต้องสิ่งเหล่านี้จะเป็นส่วนเสริมสำหรับการจัดเรียงสแต็ค

ไม่สิ่งนี้ไม่มีอะไรเกี่ยวข้องกับสแต็ก NOP ที่สร้างขึ้นตามค่าเริ่มต้นและตัวเลือก -falign - * = 1 ป้องกันสำหรับการจัดเรียงรหัส

ตามที่เหตุใด GCC pad จึงทำงานกับ NOP มันทำด้วยหวังว่ารหัสจะทำงานได้เร็วขึ้น แต่เห็นได้ชัดว่าการเพิ่มประสิทธิภาพนี้ได้ผลในกรณีของฉัน

มันเป็นช่องว่างภายในที่เป็นผู้กระทำผิดในกรณีนี้หรือไม่? ทำไมและอย่างไร

มีความเป็นไปได้สูงมากที่การรองจะเป็นตัวการ เหตุผลที่ทำให้รู้สึกว่าจำเป็นต้องมีการแพ็ดดิ้งและมีประโยชน์ในบางกรณีโดยทั่วไปแล้วรหัสนั้นจะถูกนำมาใช้ในบรรทัดที่ 16 ไบต์ (ดูทรัพยากรการปรับแต่งของ Agner Fogสำหรับรายละเอียดซึ่งแตกต่างกันไปตามรุ่นของโปรเซสเซอร์) การจัดแนวฟังก์ชัน, ลูปหรือเลเบลบนขอบเขต 16- ไบต์หมายถึงโอกาสที่เพิ่มขึ้นทางสถิติว่าจำเป็นต้องมีหนึ่งบรรทัดน้อยกว่าเพื่อให้มีฟังก์ชันหรือลูป เห็นได้ชัดว่ามันส่งคืนเนื่องจาก NOP เหล่านี้จะลดความหนาแน่นของรหัสและทำให้ประสิทธิภาพการแคช ในกรณีของลูปและเลเบล NOP อาจจำเป็นต้องดำเนินการครั้งเดียว (เมื่อการดำเนินการมาถึงลูป / ฉลากตามปกติซึ่งต่างจากการกระโดด)


สิ่งที่ตลกคือ: -O2 -fno-omit-frame-pointerก็ดีเหมือน-Osกัน โปรดตรวจสอบคำถามที่อัปเดต
อาลี

11

หากโปรแกรมของคุณถูก จำกัด โดยแคช CODE L1 ดังนั้นการปรับขนาดให้เหมาะสมจะเริ่มจ่ายทันที

เมื่อฉันตรวจสอบครั้งล่าสุดคอมไพเลอร์ไม่ฉลาดพอที่จะเข้าใจในทุกกรณี

ในกรณีของคุณ -O3 อาจสร้างรหัสเพียงพอสำหรับสองบรรทัดแคช แต่ -Os พอดีในหนึ่งบรรทัดแคช


1
คุณต้องการเดิมพันเท่าใดพารามิเตอร์ align = ที่เกี่ยวข้องกับขนาดของบรรทัดแคช?
Joshua

ฉันไม่สนใจอีกแล้ว: มันไม่ปรากฏบนเครื่องของฉัน และเมื่อผ่าน-falign-*=16ธงทุกอย่างกลับสู่ปกติทุกอย่างทำงานอย่างสม่ำเสมอ เท่าที่ฉันกังวลคำถามนี้ได้รับการแก้ไข
อาลี

7

ฉันไม่ได้หมายความว่าเป็นผู้เชี่ยวชาญในพื้นที่นี้ แต่ฉันดูเหมือนจะจำไว้ว่าโปรเซสเซอร์ที่ทันสมัยมีความสำคัญมากเมื่อมันมาถึงการทำนายสาขา อัลกอริทึมที่ใช้ในการทำนายสาขาคือ (หรืออย่างน้อยก็ย้อนกลับไปในสมัยที่ฉันเขียนโค้ดแอสเซมเบลอร์) ขึ้นอยู่กับคุณสมบัติหลายประการของรหัสรวมถึงระยะทางของเป้าหมายและทิศทาง

สถานการณ์ที่นึกได้คือลูปเล็ก ๆ เมื่อกิ่งก้านไปด้านหลังและระยะทางไม่ไกลนักกิ่งก้านสาขาจะปรับให้เหมาะสมสำหรับกรณีนี้เนื่องจากห่วงเล็ก ๆ ทั้งหมดจะทำแบบนี้ กฎเดียวกันอาจเข้ามาเล่นเมื่อคุณสลับตำแหน่งของaddและworkในรหัสที่สร้างขึ้นหรือเมื่อตำแหน่งของทั้งคู่เปลี่ยนไปเล็กน้อย

ที่กล่าวว่าฉันไม่มีความคิดวิธีการตรวจสอบว่าและฉันแค่อยากให้คุณรู้ว่านี่อาจเป็นสิ่งที่คุณต้องการตรวจสอบ


ขอบคุณ ฉันเล่นกับมัน: ฉันจะได้รับความเร็วโดยการแลกเปลี่ยนadd()และwork()ถ้า-O2ผ่านไปแล้ว ในกรณีอื่น ๆ รหัสจะช้าลงอย่างมากโดยการสลับ ในช่วงสุดสัปดาห์ฉันยังวิเคราะห์สถิติการคาดคะเนสาขา / การคาดการณ์ผิดพลาดด้วยperfและฉันไม่ได้สังเกตอะไรที่สามารถอธิบายพฤติกรรมแปลก ๆ นี้ได้ ผลลัพธ์ที่สอดคล้องกันเพียงอย่างเดียวคือในกรณีที่ช้าperfรายงาน 100.0 ในadd()และมีค่ามากในสายทันทีหลังจากที่โทรไปadd()ในวง ดูเหมือนว่าเรากำลังหยุดด้วยเหตุผลบางอย่างadd()ในกรณีที่ช้า แต่ไม่ได้อยู่ในการทำงานที่รวดเร็ว
อาลี

ฉันกำลังคิดเกี่ยวกับการติดตั้ง VTune ของ Intel ในเครื่องของฉันและทำโปรไฟล์ด้วยตนเอง perfรองรับเฉพาะบางสิ่งบางอย่างที่ จำกัด บางทีสิ่งต่างๆของ Intel นั้นค่อนข้างสะดวกในโปรเซสเซอร์ของตัวเอง
อาลี
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.