Mengapa loop sederhana dioptimalkan ketika batasnya adalah 959 tetapi tidak 960?

131

Pertimbangkan loop sederhana ini:

float f(float x[]) {
  float p = 1.0;
  for (int i = 0; i < 959; i++)
    p += 1;
  return p;
}

Jika Anda mengompilasi dengan gcc 7 (snapshot) atau dentang (trunk) dengan -march=core-avx2 -OfastAnda mendapatkan sesuatu yang sangat mirip.

.LCPI0_0:
        .long   1148190720              # float 960
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        ret

Dengan kata lain itu hanya mengatur jawaban ke 960 tanpa perulangan.

Namun, jika Anda mengubah kode menjadi:

float f(float x[]) {
  float p = 1.0;
  for (int i = 0; i < 960; i++)
    p += 1;
  return p;
}

Perakitan yang dihasilkan benar-benar melakukan jumlah loop? Misalnya dentang memberi:

.LCPI0_0:
        .long   1065353216              # float 1
.LCPI0_1:
        .long   1086324736              # float 6
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        vxorps  ymm1, ymm1, ymm1
        mov     eax, 960
        vbroadcastss    ymm2, dword ptr [rip + .LCPI0_1]
        vxorps  ymm3, ymm3, ymm3
        vxorps  ymm4, ymm4, ymm4
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        vaddps  ymm0, ymm0, ymm2
        vaddps  ymm1, ymm1, ymm2
        vaddps  ymm3, ymm3, ymm2
        vaddps  ymm4, ymm4, ymm2
        add     eax, -192
        jne     .LBB0_1
        vaddps  ymm0, ymm1, ymm0
        vaddps  ymm0, ymm3, ymm0
        vaddps  ymm0, ymm4, ymm0
        vextractf128    xmm1, ymm0, 1
        vaddps  ymm0, ymm0, ymm1
        vpermilpd       xmm1, xmm0, 1   # xmm1 = xmm0[1,0]
        vaddps  ymm0, ymm0, ymm1
        vhaddps ymm0, ymm0, ymm0
        vzeroupper
        ret

Mengapa ini dan mengapa persis sama untuk dentang dan gcc?


Batas untuk loop yang sama jika Anda ganti floatdengan doubleadalah 479. Ini sama untuk gcc dan berdering lagi.

Perbarui 1

Ternyata gcc 7 (snapshot) dan dentang (trunk) berperilaku sangat berbeda. dentang mengoptimalkan loop untuk semua batas kurang dari 960 sejauh yang saya tahu. gcc di sisi lain sensitif terhadap nilai yang tepat dan tidak memiliki batas atas. Misalnya ia tidak mengoptimalkan loop ketika batasnya adalah 200 (dan juga banyak nilai lainnya) tetapi ia melakukannya ketika batasnya adalah 202 dan 20002 (serta banyak nilai lainnya).

eleanora
sumber
3
Apa yang Sulthan mungkin maksudkan adalah bahwa 1) kompiler membuka gulungan dan 2) setelah itu terbuka melihat bahwa jumlah operasi dapat dikelompokkan menjadi satu. Jika loop tidak terbuka, operasi tidak dapat dikelompokkan.
Jean-François Fabre
3
Memiliki jumlah loop yang aneh membuat membuka gulungan lebih rumit, beberapa iterasi terakhir harus dilakukan secara khusus. Itu mungkin cukup untuk memasukkan pengoptimal ke mode di mana ia tidak lagi dapat mengenali pintasan. Kemungkinan besar, pertama-tama harus menambahkan kode untuk kasus khusus dan kemudian harus menghapusnya lagi. Menggunakan pengoptimal di antara telinga selalu yang terbaik :)
Hans Passant
3
@HansPassant Ini juga dioptimalkan untuk angka yang lebih kecil dari 959.
eleanora
6
Bukankah ini biasanya dilakukan dengan penghapusan variabel induksi, bukannya membuka gulungan jumlah yang gila? Membuka gulungan dengan faktor 959 gila.
Harold
4
@eleanora Saya bermain dengan penjelajah kompiler dan berikut ini sepertinya berlaku (berbicara tentang snapshot gcc saja): Jika jumlah loop adalah kelipatan dari 4 dan setidaknya 72, maka loop tidak terbuka (atau lebih tepatnya, tidak dikontrol oleh faktor 4); jika tidak, seluruh loop digantikan oleh konstanta - bahkan jika jumlah loop adalah 2000000001. Kecurigaan saya: optimasi prematur (seperti pada, prematur "hei, kelipatan 4, itu bagus untuk membuka gulungan" yang memblokir optimasi lebih lanjut vs. lebih teliti "Apa masalahnya dengan loop ini?")
Hagen von Eitzen

Jawaban:

88

TL; DR

Secara default, snapshot GCC 7 saat ini berperilaku tidak konsisten, sedangkan versi sebelumnya memiliki batas default karena PARAM_MAX_COMPLETELY_PEEL_TIMES, yaitu 16. Ini dapat diganti dari baris perintah.

Alasan dari batasan ini adalah untuk mencegah loop yang terlalu agresif membuka gulungan, yang bisa menjadi pedang bermata dua .

Versi GCC <= 6.3.0

Opsi pengoptimalan yang relevan untuk GCC adalah -fpeel-loops, yang diaktifkan secara tidak langsung bersamaan dengan tanda -Ofast(penekanan adalah milikku):

Peel loop yang ada informasi cukup bahwa mereka tidak banyak roll (dari umpan balik profil atau analisis statis ). Ini juga mengaktifkan peeling loop lengkap (yaitu penghapusan lengkap loop dengan jumlah iterasi yang konstan kecil ).

Diaktifkan dengan -O3dan / atau -fprofile-use.

Rincian lebih lanjut dapat diperoleh dengan menambahkan -fdump-tree-cunroll:

$ head test.c.151t.cunroll 

;; Function f (f, funcdef_no=0, decl_uid=1919, cgraph_uid=0, symbol_order=0)

Not peeling: upper bound is known so can unroll completely

Pesannya dari /gcc/tree-ssa-loop-ivcanon.c:

if (maxiter >= 0 && maxiter <= npeel)
    {
      if (dump_file)
        fprintf (dump_file, "Not peeling: upper bound is known so can "
         "unroll completely\n");
      return false;
    }

karenanya try_peel_loopfungsi kembali false.

Lebih banyak keluaran verbal dapat dicapai dengan -fdump-tree-cunroll-details:

Loop 1 iterates 959 times.
Loop 1 iterates at most 959 times.
Not unrolling loop 1 (--param max-completely-peeled-times limit reached).
Not peeling: upper bound is known so can unroll completely

Dimungkinkan untuk mengubah batas dengan melakukan plaing dengan max-completely-peeled-insns=ndan max-completely-peel-times=nparams:

max-completely-peeled-insns

Jumlah maksimum lns dari loop sepenuhnya dikupas.

max-completely-peel-times

Jumlah maksimum iterasi loop yang sesuai untuk pengelupasan lengkap.

Untuk mempelajari lebih lanjut tentang perusahaan, Anda dapat merujuk ke Manual Internal GCC .

Misalnya, jika Anda mengompilasi dengan opsi berikut:

-march=core-avx2 -Ofast --param max-completely-peeled-insns=1000 --param max-completely-peel-times=1000

lalu kode berubah menjadi:

f:
        vmovss  xmm0, DWORD PTR .LC0[rip]
        ret
.LC0:
        .long   1148207104

Dentang

Saya tidak yakin apa yang sebenarnya dilakukan Dentang dan bagaimana mengubah batas-batasnya, tetapi seperti yang saya amati, Anda bisa memaksanya untuk mengevaluasi nilai akhir dengan menandai loop dengan pragma membuka gulungan , dan itu akan menghapusnya sepenuhnya:

#pragma unroll
for (int i = 0; i < 960; i++)
    p++;

hasil menjadi:

.LCPI0_0:
        .long   1148207104              # float 961
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        ret
Grzegorz Szpetkowski
sumber
Terima kasih atas jawaban yang sangat bagus ini. Seperti yang telah ditunjukkan orang lain, gcc tampaknya peka terhadap ukuran batas yang tepat. Misalnya ia gagal menghilangkan loop untuk 912 godbolt.org/g/EQJHvT . Apa yang dikatakan detail fdump-tree-cunroll dalam kasus itu?
eleanora
Bahkan 200 bahkan memiliki masalah ini. Ini semua dalam snapshot gcc 7 yang disediakan godbolt. godbolt.org/g/Vg3SVs Ini tidak berlaku untuk dentang sama sekali.
eleanora
13
Anda menjelaskan mekanisme pengelupasan, tetapi bukan apa relevansinya dari 960 atau mengapa ada batasan sama sekali
MM
1
@ MM: Perilaku mengelupas benar-benar berbeda antara GCC 6.3.0 dan snaphost terbaru. Dalam kasus yang pertama, saya sangat curiga, bahwa batas kode keras ditegakkan oleh PARAM_MAX_COMPLETELY_PEEL_TIMESparam, yang didefinisikan /gcc/params.def:321dengan nilai 16.
Grzegorz Szpetkowski
14
Anda mungkin ingin menyebutkan mengapa GCC sengaja membatasi diri dengan cara ini. Khususnya, jika Anda membuka gulungan terlalu agresif, biner menjadi lebih besar dan Anda cenderung masuk ke cache L1. Kehilangan cache berpotensi cukup mahal dibandingkan dengan menyimpan beberapa lompatan bersyarat, dengan asumsi prediksi cabang yang baik (yang akan Anda miliki, untuk perulangan tipikal).
Kevin
19

Setelah membaca komentar Sulthan, saya kira itu:

  1. Compiler sepenuhnya membuka gulungan loop jika penghitung loop konstan (dan tidak terlalu tinggi)

  2. Setelah dibuka, kompiler melihat bahwa operasi penjumlahan dapat dikelompokkan menjadi satu.

Jika loop tidak terbuka untuk beberapa alasan (di sini: itu akan menghasilkan terlalu banyak pernyataan dengan 1000), operasi tidak dapat dikelompokkan.

Kompilator dapat melihat bahwa membuka gulungan dari 1000 pernyataan berjumlah satu tambahan, tetapi langkah 1 & 2 yang dijelaskan di atas adalah dua optimisasi terpisah, sehingga tidak dapat mengambil "risiko" membuka gulungan, tidak tahu apakah operasi dapat dikelompokkan (contoh: panggilan fungsi tidak dapat dikelompokkan).

Catatan: Ini adalah kasus sudut: Siapa yang menggunakan loop untuk menambahkan hal yang sama lagi? Dalam hal ini, jangan bergantung pada kompiler yang memungkinkan membuka gulungan / mengoptimalkan; langsung tulis operasi yang tepat dalam satu instruksi.

Jean-François Fabre
sumber
1
maka bisakah Anda fokus pada not too highbagian itu? Maksud saya mengapa risikonya tidak ada jika ada 100? Saya telah menebak sesuatu ... dalam komentar saya di atas .. apakah itu bisa menjadi alasan untuk itu?
user2736738
Saya pikir bahwa kompiler tidak menyadari ketidaktepatan floating point yang dapat memicu. Saya kira itu hanya batas ukuran instruksi. Anda ada max-unrolled-insnsbersamamax-unrolled-times
Jean-François Fabre
Ah itu semacam pemikiran atau dugaanku ... ingin mendapatkan alasan yang lebih jelas.
user2736738
5
Menariknya jika Anda mengubah floatke int, kompiler gcc dapat mengurangi kekuatan loop terlepas dari jumlah iterasi, karena optimasi variabel induksi ( -fivopts). Tapi sepertinya itu tidak berhasil float.
Tavian Barnes
1
@CortAmmon Benar, dan saya ingat pernah membaca beberapa orang yang terkejut dan kesal bahwa GCC menggunakan MPFR untuk secara tepat menghitung angka yang sangat besar, memberikan hasil yang agak berbeda dari operasi floating point setara yang akan mengakumulasi kesalahan dan kehilangan presisi. Menunjukkan bahwa banyak orang menghitung floating point dengan cara yang salah.
Zan Lynx
12

Pertanyaan yang sangat bagus

Anda tampaknya telah mencapai batas pada jumlah iterasi atau operasi yang coba dilakukan oleh kompiler saat menyederhanakan kode. Seperti yang didokumentasikan oleh Grzegorz Szpetkowski, ada cara khusus kompiler untuk mengubah batas ini dengan pragma atau opsi baris perintah.

Anda juga dapat bermain dengan Explorer Kompiler Godbolt untuk membandingkan bagaimana berbagai kompiler dan opsi berdampak pada kode yang dihasilkan: gcc 6.2dan icc 17masih sebaris kode untuk 960, sedangkan clang 3.9tidak (dengan konfigurasi default Godbolt, itu sebenarnya berhenti sebaris di 73).

chqrlie
sumber
Saya telah mengedit pertanyaan untuk menjelaskan versi gcc dan dentang yang saya gunakan. Lihat godbolt.org/g/FfwWjL . Saya menggunakan -Ofast misalnya.
eleanora