Mengapa GCC menghasilkan kode 15-20% lebih cepat jika saya mengoptimalkan ukuran bukan kecepatan?

445

Saya pertama kali memperhatikan pada tahun 2009 bahwa GCC (setidaknya pada proyek saya dan pada mesin saya) memiliki kecenderungan untuk menghasilkan kode yang lebih cepat jika saya mengoptimalkan untuk ukuran ( -Os) daripada kecepatan ( -O2atau -O3), dan saya telah bertanya-tanya sejak mengapa.

Saya telah berhasil membuat kode (agak konyol) yang menunjukkan perilaku mengejutkan ini dan cukup kecil untuk diposting di sini.

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

Jika saya mengompilasinya -Os, dibutuhkan 0,38 detik untuk menjalankan program ini, dan 0,44 detik jika dikompilasi dengan -O2atau -O3. Waktu-waktu ini diperoleh secara konsisten dan praktis tanpa noise (gcc 4.7.2, x86_64 GNU / Linux, Intel Core i5-3320M).

(Pembaruan: Saya telah memindahkan semua kode assembly ke GitHub : Mereka membuat postingan membengkak dan tampaknya menambah sedikit nilai pada pertanyaan karena fno-align-*flag - flag memiliki efek yang sama.)

Ini adalah rakitan yang dihasilkan dengan -Osdan -O2.

Sayangnya, pemahaman saya tentang perakitan sangat terbatas, jadi saya tidak tahu apakah apa yang saya lakukan berikutnya adalah benar: Saya meraih perakitan untuk -O2dan bergabung semua perbedaan ke dalam perakitan untuk -Os kecuali yang .p2aligngaris, hasil di sini . Kode ini masih berjalan di 0.38s dan satu-satunya perbedaan adalah .p2align barang.

Jika saya menebak dengan benar, ini adalah paddings untuk perataan tumpukan. Menurut Mengapa fungsi pad GCC dengan NOP? itu dilakukan dengan harapan kode akan berjalan lebih cepat, tetapi ternyata optimasi ini menjadi bumerang dalam kasus saya.

Apakah ini padding yang menjadi pelakunya? Kenapa dan bagaimana?

Kebisingan yang dihasilkannya membuat pengoptimalan mikro menjadi tidak mungkin.

Bagaimana saya bisa memastikan bahwa keberpihakan yang tidak disengaja / tidak beruntung tersebut tidak mengganggu ketika saya melakukan optimasi mikro (tidak terkait dengan penumpukan keselarasan) pada kode sumber C atau C ++?


MEMPERBARUI:

Mengikuti jawaban Pascal Cuoq, saya mengutak-atik sedikit dengan keberpihakan. Dengan meneruskan -O2 -fno-align-functions -fno-align-loopske gcc, semua .p2alignhilang dari perakitan dan menjalankan eksekusi yang dihasilkan di 0,38s. Menurut dokumentasi gcc :

-Os memungkinkan semua -O2 optimasi [tetapi] -Os menonaktifkan flag optimasi berikut:

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

Jadi, sepertinya masalah pelurusan (mis).

Saya masih skeptis tentang -march=nativeseperti yang disarankan dalam jawaban Marat Dukhan . Saya tidak yakin bahwa itu tidak hanya mengganggu masalah keberpihakan (salah) ini; sama sekali tidak berpengaruh pada mesin saya. (Namun demikian, saya meningkatkan jawabannya.)


PEMBARUAN 2:

Kita bisa mengambil -Osgambarnya. Waktu berikut diperoleh dengan kompilasi dengan

  • -O2 -fno-omit-frame-pointer 0,37 detik

  • -O2 -fno-align-functions -fno-align-loops 0,37 detik

  • -S -O2kemudian secara manual memindahkan perakitan add()setelah work()0,37s

  • -O2 0,44

Sepertinya bagi saya jarak dari add()situs panggilan sangat penting. Saya sudah mencoba perf, tetapi hasil dari perf statdan perf reportsangat tidak masuk akal bagi saya. Namun, saya hanya bisa mendapatkan satu hasil yang konsisten dari itu:

-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

Untuk 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
       ¦    }

Untuk -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

Sepertinya kita menunda panggilan untuk add()dalam kasus lambat.

Saya telah memeriksa segala sesuatu yang perf -edapat dimuntahkan di mesin saya; bukan hanya statistik yang diberikan di atas.

Untuk executable yang sama, stalled-cycles-frontendmenunjukkan korelasi linier dengan waktu eksekusi; Saya tidak memperhatikan hal lain yang akan berkorelasi begitu jelas. (Membandingkan stalled-cycles-frontenduntuk executable yang berbeda tidak masuk akal bagi saya.)

Saya memasukkan cache yang terlewat ketika muncul sebagai komentar pertama. Saya memeriksa semua kesalahan cache yang dapat diukur pada mesin saya perf, bukan hanya yang diberikan di atas. Tembolok yang hilang sangat bising dan tidak menunjukkan korelasi dengan waktu eksekusi.

Ali
sumber
36
Tebakan buta: dapatkah ini menjadi cache miss?
@ H2CO3 Itu adalah pemikiran pertama saya juga, tetapi tidak cukup didorong untuk memposting komentar tanpa membaca dan memahami pertanyaan OP secara mendalam.
πάντα ῥεῖ
2
@ g-makulik Itu sebabnya saya memperingatkan bahwa itu adalah "tebakan buta" ;-) "TL; DR" dicadangkan untuk pertanyaan buruk. : P
3
Hanya titik data yang menarik: Saya menemukan bahwa -O3 atau -Ofast sekitar 1,5x lebih cepat dari -Os ketika saya kompilasi ini dengan dentang pada OS X. (Saya belum mencoba mereproduksi dengan gcc.)
Rob Napier
2
Itu kode yang sama. Lihatlah lebih dekat alamat .L3, target cabang yang tidak selaras mahal.
Hans Passant

Jawaban:

504

Secara default, kompiler mengoptimalkan untuk prosesor "rata-rata". Karena prosesor yang berbeda menyukai urutan instruksi yang berbeda, optimisasi kompiler yang diaktifkan oleh -O2prosesor mungkin menguntungkan, tetapi menurunkan kinerja prosesor khusus Anda (dan hal yang sama berlaku untuk -Os). Jika Anda mencoba contoh yang sama pada prosesor yang berbeda, Anda akan menemukan bahwa pada beberapa dari mereka mendapat manfaat dari -O2sementara yang lain lebih disukai untuk -Osoptimasi.

Berikut adalah hasil untuk time ./test 0 0beberapa prosesor (waktu pengguna dilaporkan):

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

Dalam beberapa kasus, Anda dapat mengurangi efek optimasi yang tidak menguntungkan dengan meminta gccmengoptimalkan prosesor khusus Anda (menggunakan opsi -mtune=nativeatau -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

Update: di Core berbasis Ivy Bridge i3 tiga versi gcc( 4.6.4, 4.7.3, dan 4.8.1) binari hasil dengan kinerja yang berbeda secara signifikan, tapi kode assembly memiliki variasi hanya halus. Sejauh ini, saya tidak punya penjelasan tentang fakta ini.

Majelis dari gcc-4.6.4 -Os(dijalankan dalam 0,709 detik):

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

Majelis dari gcc-4.7.3 -Os(dijalankan dalam 0,822 detik):

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

Majelis dari gcc-4.8.1 -Os(dijalankan dalam 0,994 detik):

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
Marat Dukhan
sumber
186
Hanya untuk memperjelas: apakah Anda benar-benar pergi dan mengukur kinerja kode OP pada 12 platform yang berbeda? (+1 hanya untuk pemikiran bahwa Anda akan melakukan itu)
anatolyg
194
@anatolyg Ya, saya lakukan! (dan akan menambah beberapa lagi segera)
Marat Dukhan
43
Memang. +1 lain untuk tidak hanya berteori tentang CPU yang berbeda tetapi sebenarnya membuktikannya . Bukan sesuatu (sayangnya) yang Anda lihat dalam setiap jawaban terkait kecepatan. Apakah tes ini dijalankan dengan OS yang sama? (Mungkin saja hal ini mengacaukan hasilnya ...)
usr2564301
7
@ Ali On AMD-FX 6300 -O2 -fno-align-functions -fno-align-loopsturun waktu 0.340s, jadi itu bisa dijelaskan dengan perataan. Namun, penyelarasan optimal tergantung pada prosesor: beberapa prosesor lebih memilih loop dan fungsi yang selaras.
Marat Dukhan
13
@ Jongware Saya tidak melihat bagaimana OS akan mempengaruhi hasil secara signifikan; loop tidak pernah melakukan panggilan sistem.
Ali
186

Rekan saya membantu saya menemukan jawaban yang masuk akal untuk pertanyaan saya. Dia memperhatikan pentingnya batas 256 byte. Dia tidak terdaftar di sini dan mendorong saya untuk mengirim jawaban sendiri (dan menerima semua ketenaran).


Jawaban singkat:

Apakah ini padding yang menjadi pelakunya? Kenapa dan bagaimana?

Semuanya bermuara pada perataan. Penyelarasan dapat memiliki dampak signifikan pada kinerja, itu sebabnya kami memiliki -falign-*bendera di tempat pertama.

Saya telah mengirimkan laporan bug (palsu?) Kepada pengembang gcc . Ternyata perilaku default adalah "kami menyelaraskan loop ke 8 byte secara default tetapi mencoba untuk menyelaraskannya ke 16 byte jika kita tidak perlu mengisi lebih dari 10 byte." Rupanya, default ini bukan pilihan terbaik dalam kasus khusus ini dan di komputer saya. Dentang 3.4 (trunk) dengan -O3apakah keselarasan yang sesuai dan kode yang dihasilkan tidak menunjukkan perilaku aneh ini.

Tentu saja, jika penyelarasan yang tidak tepat dilakukan, itu memperburuk keadaan. Perataan yang tidak perlu / buruk hanya memakan byte tanpa alasan dan berpotensi meningkatkan kesalahan cache, dll.

Kebisingan yang dihasilkannya membuat pengoptimalan mikro menjadi tidak mungkin.

Bagaimana saya bisa memastikan bahwa keberpihakan yang tidak disengaja / tidak beruntung tersebut tidak mengganggu ketika saya melakukan optimasi mikro (tidak terkait dengan penumpukan keselarasan) pada kode sumber C atau C ++?

Cukup dengan memberi tahu gcc untuk melakukan perataan yang benar:

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


Jawaban panjang:

Kode akan berjalan lebih lambat jika:

  • sebuah XXbyte pemotongan batas add()di tengah ( XXyang mesin tergantung).

  • jika panggilan ke add()harus melompati XXbatas byte dan target tidak selaras.

  • jika add()tidak selaras.

  • jika loop tidak selaras.

2 pertama terlihat indah pada kode dan hasil yang diposting Marat Dukhan . Dalam hal ini, gcc-4.8.1 -Os(dijalankan dalam 0,994 detik):

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

batas 256 byte memotong add()tepat di tengah dan tidak satu add()pun loop selaras. Kejutan, kejutan, ini adalah kasus paling lambat!

Dalam kasus gcc-4.7.3 -Os(dijalankan dalam 0,822 detik), batas 256 byte hanya memotong bagian yang dingin (tetapi tidak loop, juga add()tidak dipotong):

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>

Tidak ada yang diselaraskan, dan panggilan ke add()harus melompati batas 256 byte. Kode ini adalah yang paling lambat kedua.

Dalam kasus gcc-4.6.4 -Os(dijalankan dalam 0,709 detik), meskipun tidak ada yang selaras, panggilan untuk add()tidak harus melompati batas 256 byte dan targetnya persis 32 byte jauhnya:

  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>

Ini adalah yang tercepat dari ketiganya. Mengapa batas 256 byte adalah speacial pada mesinnya, saya akan menyerahkannya kepadanya untuk mengetahuinya. Saya tidak punya prosesor seperti itu.

Sekarang, pada mesin saya, saya tidak mendapatkan efek batas 256 byte ini. Hanya fungsi dan penyelarasan loop menendang pada mesin saya. Jika saya lulus g++ -O2 -falign-functions=16 -falign-loops=16maka semuanya kembali normal: Saya selalu mendapatkan case tercepat dan waktu tidak sensitif -fno-omit-frame-pointerlagi terhadap flag. Saya bisa lulus g++ -O2 -falign-functions=32 -falign-loops=32atau kelipatan 16, kode tidak peka untuk itu.

Saya pertama kali memperhatikan pada tahun 2009 bahwa gcc (setidaknya pada proyek saya dan pada mesin saya) memiliki kecenderungan untuk menghasilkan kode yang lebih cepat jika saya mengoptimalkan untuk ukuran (-O) daripada kecepatan (-O2 atau -O3) dan saya bertanya-tanya sejak mengapa.

Penjelasan yang mungkin adalah bahwa saya memiliki hotspot yang sensitif terhadap perataan, seperti yang ada dalam contoh ini. Dengan mengacaukan bendera (lewat -Osbukan -O2), titik-titik panas itu disejajarkan secara kebetulan dan kodenya menjadi lebih cepat. Itu tidak ada hubungannya dengan mengoptimalkan ukuran: Ini adalah kebetulan bahwa hotspot menjadi lebih baik. Mulai sekarang, saya akan memeriksa efek penyelarasan pada proyek saya.

Oh, dan satu hal lagi. Bagaimana hotspot seperti itu muncul, seperti yang ditunjukkan dalam contoh? Bagaimana bisa inlining dari fungsi sekecil itu seperti add()gagal?

Pertimbangkan ini:

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

dan dalam file terpisah:

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

dan dikompilasi sebagai: g++ -O2 add.cpp main.cpp.

      gcc tidak akan terhubung add()!

Itu saja, semudah itu untuk membuat hotspot tanpa sengaja seperti yang ada di OP. Tentu saja itu sebagian kesalahan saya: gcc adalah kompiler yang sangat baik. Jika kompilasi di atas sebagai :,g++ -O2 -flto add.cpp main.cpp yaitu, jika saya melakukan optimasi waktu tautan, kode tersebut berjalan di 0.19s!

(Inlining secara artifisial dinonaktifkan dalam OP, karenanya, kode dalam OP itu 2x lebih lambat).

Ali
sumber
19
Wow ... Ini jelas melampaui apa yang biasanya saya lakukan untuk mengatasi anomali pembandingan.
Mysticial
@Ali Saya kira itu masuk akal karena bagaimana kompiler bisa menyejajarkan sesuatu yang tidak dilihatnya? Mungkin itulah sebabnya kami menggunakan inlinedefinisi fungsi + di header. Tidak yakin seberapa dewasa lto di gcc. Pengalaman saya dengannya setidaknya di mingw adalah hit atau miss.
greatwolf
7
Saya pikir itu adalah Komunikasi ACM yang memiliki artikel beberapa tahun yang lalu tentang menjalankan aplikasi yang cukup besar (perl, Spice, dll.) Sambil menggeser keseluruhan gambar biner satu byte pada suatu waktu dengan menggunakan lingkungan Linux dengan ukuran yang berbeda. Saya ingat varian tipikal 15% atau lebih. Ringkasan mereka adalah bahwa banyak hasil benchmark tidak berguna karena variabel eksternal penyelarasan ini tidak diperhitungkan.
Gene
1
terutama untuk -flto. itu cukup revolusioner jika Anda belum pernah menggunakannya sebelumnya, berbicara dari pengalaman :)
underscore_d
2
Ini adalah video fantastis yang membahas tentang bagaimana penyelarasan dapat memengaruhi kinerja dan cara membuat profil untuk itu: youtube.com/watch?time_continue=1&v=r-TLSBdHe1A
Zhro
73

Saya menambahkan post-accept ini untuk menunjukkan bahwa efek penyelarasan pada kinerja keseluruhan program - termasuk yang besar - telah dipelajari. Sebagai contoh, artikel ini (dan saya percaya versi ini juga muncul di CACM) menunjukkan bagaimana urutan tautan dan perubahan ukuran lingkungan OS saja sudah cukup untuk mengubah kinerja secara signifikan. Mereka mengaitkan ini dengan penyelarasan "hot loop".

Makalah ini, berjudul "Menghasilkan data yang salah tanpa melakukan sesuatu yang jelas salah!" mengatakan bahwa bias eksperimental yang tidak disengaja karena perbedaan yang hampir tidak terkendali dalam lingkungan menjalankan program mungkin membuat banyak hasil benchmark menjadi tidak berarti.

Saya pikir Anda menemukan sudut yang berbeda pada pengamatan yang sama.

Untuk kode kritis-kinerja, ini adalah argumen yang cukup bagus untuk sistem yang menilai lingkungan pada saat instalasi atau waktu berjalan dan memilih yang terbaik di antara versi rutin utama kunci yang dioptimalkan.

Gene
sumber
33

Saya pikir Anda dapat memperoleh hasil yang sama seperti yang Anda lakukan:

Saya meraih rakitan untuk -O2 dan menggabungkan semua perbedaannya menjadi rakitan untuk -O kecuali baris .p2align:

... dengan menggunakan -O2 -falign-functions=1 -falign-jumps=1 -falign-loops=1 -falign-labels=1. Saya telah mengkompilasi semuanya dengan opsi-opsi ini, yang lebih cepat daripada biasa -O2setiap kali saya repot-repot mengukur, selama 15 tahun.

Juga, untuk konteks yang sama sekali berbeda (termasuk kompiler yang berbeda), saya perhatikan bahwa situasinya serupa : opsi yang seharusnya “mengoptimalkan ukuran kode daripada kecepatan” mengoptimalkan untuk ukuran dan kecepatan kode.

Jika saya menebak dengan benar, ini adalah paddings untuk perataan tumpukan.

Tidak, ini tidak ada hubungannya dengan stack, NOP yang dihasilkan secara default dan opsi -falign - * = 1 mencegah adalah untuk penyelarasan kode.

Menurut Mengapa fungsi pad GCC dengan NOP? itu dilakukan dengan harapan kode akan berjalan lebih cepat tetapi ternyata optimasi ini menjadi bumerang dalam kasus saya.

Apakah ini padding yang menjadi pelakunya? Kenapa dan bagaimana?

Sangat mungkin bahwa bantalan adalah pelakunya. Alasan padding dirasakan perlu dan berguna dalam beberapa kasus adalah bahwa kode biasanya diambil dalam garis 16 byte (lihat sumber optimasi Agner Fog untuk detailnya, yang bervariasi tergantung model prosesor). Menyelaraskan suatu fungsi, loop, atau label pada batas 16-byte berarti bahwa peluang secara statistik meningkat bahwa satu garis lebih sedikit akan diperlukan untuk memuat fungsi atau loop. Jelas, itu menjadi bumerang karena NOP ini mengurangi kepadatan kode dan karenanya efisiensi cache. Dalam kasus loop dan label, NOP bahkan mungkin perlu dieksekusi sekali (ketika eksekusi tiba ke loop / label secara normal, sebagai lawan dari lompatan).

Pascal Cuoq
sumber
Yang lucu adalah: -O2 -fno-omit-frame-pointersama baiknya dengan -Os. Silakan periksa pertanyaan yang diperbarui.
Ali
11

Jika program Anda dibatasi oleh cache CODE L1, maka mengoptimalkan ukuran secara tiba-tiba mulai terbayar.

Ketika saya terakhir memeriksa, kompiler tidak cukup pintar untuk mengetahui hal ini dalam semua kasus.

Dalam kasus Anda, -O3 mungkin menghasilkan kode yang cukup untuk dua baris cache, tetapi -Os cocok dalam satu baris cache.

Joshua
sumber
1
Seberapa besar Anda ingin bertaruh bahwa parameter align = terkait dengan ukuran garis cache?
Joshua
Saya tidak peduli lagi: Tidak terlihat di mesin saya. Dan dengan melewati -falign-*=16bendera, semuanya kembali normal, semuanya berperilaku konsisten. Sejauh yang saya ketahui, pertanyaan ini diselesaikan.
Ali
7

Saya sama sekali tidak ahli dalam bidang ini, tetapi saya sepertinya ingat bahwa prosesor modern cukup sensitif ketika datang ke prediksi cabang . Algoritma yang digunakan untuk memprediksi cabang adalah (atau setidaknya kembali pada hari-hari saya menulis kode assembler) berdasarkan pada beberapa properti kode, termasuk jarak target dan arah.

Skenario yang muncul dalam pikiran adalah loop kecil. Ketika cabang mundur dan jaraknya tidak terlalu jauh, kecenderungan cabang mengoptimalkan untuk kasus ini karena semua loop kecil dilakukan dengan cara ini. Aturan yang sama mungkin ikut bermain ketika Anda menukar lokasi adddan workdalam kode yang dihasilkan atau ketika posisi keduanya sedikit berubah.

Karena itu, saya tidak tahu cara memverifikasi itu dan saya hanya ingin memberi tahu Anda bahwa ini mungkin sesuatu yang ingin Anda periksa.

Daniel Frey
sumber
Terima kasih. Saya bermain dengannya: Saya hanya mendapatkan kecepatan dengan bertukar add()dan work()jika -O2dilewati. Dalam semua kasus lain, kode menjadi lebih lambat secara signifikan dengan bertukar. Selama akhir minggu, saya juga menganalisis statistik prediksi / mis-prediksi cabang perfdan saya tidak melihat apa pun yang bisa menjelaskan perilaku aneh ini. Satu-satunya hasil yang konsisten adalah bahwa dalam kasus lambat perfmelaporkan 100,0 add()dan nilai besar di telepon setelah panggilan ke add()dalam loop. Sepertinya kita mengulur-ulur untuk beberapa alasan add()dalam kasus lambat tetapi tidak dalam menjalankan cepat.
Ali
Saya berpikir untuk menginstal Intel VTune di salah satu komputer saya dan melakukan profiling sendiri. perfhanya mendukung sejumlah hal, mungkin barang-barang Intel sedikit lebih berguna pada prosesor mereka sendiri.
Ali