Sınır 959 iken 960 değilken neden basit bir döngü optimize edilir?


131

Şu basit döngüyü düşünün:

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

Gcc 7 (anlık görüntü) veya clang (trunk) ile derlerseniz, -march=core-avx2 -Ofastçok benzer bir şey elde edersiniz.

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

Başka bir deyişle, cevabı döngü yapmadan 960'a ayarlar.

Ancak kodu şu şekilde değiştirirseniz:

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

Üretilen derleme aslında döngü toplamını gerçekleştirir Örneğin clang şunu verir:

.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

Bu neden ve neden clang ve gcc için tamamen aynı?


Aynı döngü sınırı değiştirirseniz floatile doublebu yine gcc ve clang için aynıdır 479. olduğunu.

Güncelleme 1

Gcc 7'nin (anlık görüntü) ve clang'ın (trunk) çok farklı davrandığı ortaya çıktı. clang, anlayabildiğim kadarıyla 960'ın altındaki tüm sınırlar için döngüleri optimize ediyor. gcc ise tam değere duyarlıdır ve bir üst limiti yoktur. Örneğin o etmez sınırı 200 olduğu zaman döngü üzerinden duruma (aynı zamanda pek çok diğer değerler) ama yapar sınır 202 ve 20002 (aynı zamanda pek çok diğer değerleri) olduğunda.


3
Sulthan'ın muhtemelen anlamı şudur: 1) derleyici döngüyü açar ve 2) bir kez kaydı kaldırıldıktan sonra toplam işlemlerinin bir grupta gruplanabileceğini görür. Döngü açılmamışsa, işlemler gruplanamaz.
Jean-François Fabre

3
Tek sayıda döngüye sahip olmak, açmayı daha karmaşık hale getirir, son birkaç yineleme özel olarak yapılmalıdır. Bu, optimize ediciyi artık kısayolu tanıyamayacağı bir moda sokmak için yeterli olabilir. Büyük olasılıkla, önce özel durum için kod eklemesi ve ardından tekrar kaldırması gerekir. Optimize
ediciyi

3
@HansPassant Ayrıca 959'dan küçük herhangi bir sayı için optimize edilmiştir.
eleanora

6
Bu genellikle çılgın bir miktarı açmak yerine tümevarım değişken eliminasyonuyla yapılmaz mıydı? 959 çarpanıyla oyundan çıkmak çılgınlık.
harold

4
@eleanora O compilre kaşif ile oynanan ve aşağıdaki beklemeye görünüyor (gcc anlık bahsediyor için): döngü sayımı 4 çoklu ve en az 72, daha sonra döngü olduğunu ise değil sargısı (ya da daha doğrusu, a ile unrolled faktör 4); aksi takdirde, döngü sayısı 2000000001 olsa bile, tüm döngü bir sabitle değiştirilir. Şüphem: erken optimizasyon (olduğu gibi, erken "hey, 4'ün katı, bu, daha fazla optimizasyonu engelleyen" daha kapsamlı "Bu döngüde sorun nedir?")
Hagen von Eitzen

Yanıtlar:


88

TL; DR

Varsayılan olarak, mevcut anlık görüntü GCC 7 tutarsız davranır, önceki sürümlerde PARAM_MAX_COMPLETELY_PEEL_TIMESise 16 olan varsayılan sınır vardır. Bu, komut satırından geçersiz kılınabilir.

Sınırın mantığı, iki ucu keskin bir kılıç olabilecek çok agresif döngü açılmasını önlemektir .

GCC sürümü <= 6.3.0

GCC için ilgili optimizasyon seçeneği -fpeel-loops, bayrakla birlikte dolaylı olarak etkinleştirilen -Ofast(vurgu benimdir):

Çok fazla yuvarlanmadıkları yeterli bilginin bulunduğu (profil geri bildirimi veya statik analizden ) döngüleri soyar . Ayrıca, tam döngü soyulmasını da açar (yani , sabit sayıda yineleme ile döngülerin tamamen kaldırılması ).

-O3Ve / veya ile etkinleştirildi -fprofile-use.

Ekleyerek daha fazla ayrıntı elde edilebilir -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

Mesaj şu kişiden geliyor /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;
    }

dolayısıyla try_peel_loopfonksiyon geri döner false.

Daha ayrıntılı çıktıya aşağıdakilerle ulaşılabilir -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

max-completely-peeled-insns=nVe max-completely-peel-times=nparams ile limitleri değiştirmek mümkündür:

max-completely-peeled-insns

Tamamen soyulmuş bir döngünün maksimum insn sayısı.

max-completely-peel-times

Tam soyulmaya uygun bir döngünün maksimum yineleme sayısı.

İnsns hakkında daha fazla bilgi edinmek için GCC Internals Manual'a başvurabilirsiniz .

Örneğin, aşağıdaki seçeneklerle derlerseniz:

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

sonra kod şuna dönüşür:

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

çınlama

Clang'ın gerçekte ne yaptığından ve sınırlarını nasıl değiştireceğinden emin değilim, ancak gözlemlediğim gibi, döngüyü unroll pragma ile işaretleyerek onu nihai değeri değerlendirmeye zorlayabilirsiniz ve bu onu tamamen kaldıracaktır:

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

sonuçlanır:

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

Bu güzel cevap için teşekkür ederim. Diğerlerinin de belirttiği gibi, gcc kesin sınır boyutuna duyarlı görünmektedir. Örneğin, 912 godbolt.org/g/EQJHvT döngüsünü ortadan kaldıramaz . Fdump-tree-cunroll-details bu durumda ne diyor?
eleanora

Aslında 200 bile bu sorunu yaşıyor. Bunların hepsi, godbolt'un sağladığı gcc 7'nin anlık görüntüsünde. godbolt.org/g/Vg3SVs Bu, clang için hiç geçerli değildir.
eleanora

13
Soymanın mekaniğini açıklıyorsunuz, ancak 960'ın alaka düzeyinin ne olduğunu veya neden bir sınır olduğunu açıklamıyorsunuz
MM

1
@MM: Soyulma davranışı GCC 6.3.0 ve en son snaphost arasında tamamen farklıdır. İlki durumunda, sabit kodlu sınırın 16 değeriyle PARAM_MAX_COMPLETELY_PEEL_TIMEStanımlanan param tarafından uygulandığından kuvvetle şüpheleniyorum /gcc/params.def:321.
Grzegorz Szpetkowski

14
GCC'nin neden kendisini kasıtlı olarak bu şekilde sınırladığını belirtmek isteyebilirsiniz . Özellikle, döngülerinizi çok agresif bir şekilde açarsanız, ikili kod büyür ve L1 önbelleğine sığma olasılığınız azalır. Önbellek atlamaları, birkaç koşullu atlamayı kurtarmaya göre potansiyel olarak oldukça pahalıdır ve iyi dallanma tahmini (tipik bir döngü için sahip olacağınız) varsayılır.
Kevin

19

Sulthan'ın yorumunu okuduktan sonra sanırım:

  1. Derleyici, döngü sayacı sabitse (ve çok yüksek değilse) döngüyü tamamen açar

  2. Bir kez kaydı kaldırıldığında, derleyici toplam işlemlerinin tek bir işlemde gruplanabileceğini görür.

Döngü herhangi bir nedenle açılmazsa (burada: ile çok fazla ifade oluşturur 1000), işlemler gruplanamaz.

Derleyici olabilir 1000 tabloların göz önüne sermek tek ek yaratmak anlamına geldiği görüyorum ama adım 1 ve 2 bu işlemler gruplandırılabilir if (örnek bilmeden unrolling ait "risk" alamaz böylece yukarıdaki iki ayrı optimizasyonları de tarif edilmiştir: bir işlev çağrısı gruplanamaz).

Not: Bu bir köşe vakası: Aynı şeyi tekrar eklemek için bir döngüyü kim kullanır? Bu durumda, derleyicinin olası unroll / optimizasyonuna güvenmeyin; doğrudan doğru işlemi bir talimatta yazın.


1
o zaman o not too highkısma odaklanabilir misin ? Demek istediğim, neden risk yok 100? Yukarıdaki yorumumda bir şey tahmin ettim ... bunun nedeni olabilir mi?
user2736738

Derleyicinin, tetikleyebileceği kayan nokta yanlışlığının farkında olmadığını düşünüyorum. Sanırım bu sadece bir talimat boyutu sınırı. You have max-unrolled-insnsyanındamax-unrolled-times
Jean-François Fabre

Ah bu benim düşüncem ya da tahminim gibiydi ... daha net bir akıl yürütmek istiyorum
user2736738

5
İlginç floatbir şekilde int, bir gcc derleyicisini değiştirirseniz, indüksiyon değişkeni optimizasyonları ( -fivopts) nedeniyle, yineleme sayısından bağımsız olarak döngüyü güç-azaltabilir . Ama bunlar floats için işe yaramıyor gibi görünüyor .
Tavian Barnes

1
@CortAmmon Right, ve GCC'nin çok büyük sayıları hassas bir şekilde hesaplamak için MPFR kullanmasına şaşıran ve üzülen bazı insanları okuduğumu hatırlıyorum, bu da birikmiş hata ve hassasiyet kaybı olan eşdeğer kayan nokta işlemlerinden oldukça farklı sonuçlar veriyor. Birçok kişinin kayan noktayı yanlış şekilde hesapladığını gösterecek.
Zan Lynx

12

Çok güzel soru!

Derleyicinin kodu basitleştirirken satır içi yapmaya çalıştığı yineleme veya işlem sayısında bir sınıra ulaşmış görünüyorsunuz. Grzegorz Szpetkowski tarafından belgelendiği gibi, bu sınırları pragmalar veya komut satırı seçenekleriyle ayarlamanın derleyiciye özgü yolları vardır.

Ayrıca, farklı derleyicilerin ve seçeneklerin üretilen kodu nasıl etkilediğini karşılaştırmak için Godbolt's Compiler Explorer ile de oynayabilirsiniz : gcc 6.2ve icc 17yine de 960 için kodu satır içi, oysa clang 3.9değil (varsayılan Godbolt yapılandırmasıyla, aslında satır içi 73'te durur).


Soruyu, kullandığım gcc ve clang sürümlerini netleştirmek için düzenledim. Godbolt.org/g/FfwWjL adresine bakın . Örneğin -Ofast kullanıyorum.
eleanora
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.