Bana öyle geliyor ki insanlar bir goto
ifadeden çok hoşlanmıyorlar , bu yüzden bunu biraz düzeltmem gerektiğini hissettim.
İnsanların goto
eninde sonunda sahip oldukları 'duyguların' kodun anlaşılmasıyla ve olası performans sonuçlarıyla ilgili (yanlış anlamalar) kaybolacağına inanıyorum . Bu soruyu cevaplamadan önce, nasıl derlendiğine dair bazı ayrıntılara gireceğim.
Hepimizin bildiği gibi, C # IL'ye derlenir ve daha sonra bir SSA derleyicisi kullanarak montajcıya derlenir. Tüm bunların nasıl çalıştığı hakkında biraz bilgi vereceğim ve ardından sorunun kendisine cevap vermeye çalışacağım.
C # 'dan IL' ye
İlk önce bir parça C # koduna ihtiyacımız var. Basit başlayalım:
foreach (var item in array)
{
// ...
break;
// ...
}
Kaputun altında neler olduğu hakkında iyi bir fikir vermek için bunu adım adım yapacağım.
İlk çeviri: den foreach
eşdeğer for
döngüye (Not: Burada bir dizi kullanıyorum, çünkü IDisposable'ın ayrıntılarına girmek istemiyorum - bu durumda da bir IEnumerable kullanmak zorunda kalacağım):
for (int i=0; i<array.Length; ++i)
{
var item = array[i];
// ...
break;
// ...
}
İkinci çeviri: for
ve break
, daha kolay bir karşılığıdır:
int i=0;
while (i < array.Length)
{
var item = array[i];
// ...
break;
// ...
++i;
}
Ve üçüncü çeviri (bu IL kod eşdeğerdir): Biz değiştirmek break
ve while
bir şube haline:
int i=0; // for initialization
startLoop:
if (i >= array.Length) // for condition
{
goto exitLoop;
}
var item = array[i];
// ...
goto exitLoop; // break
// ...
++i; // for post-expression
goto startLoop;
Derleyici bunları tek bir adımda gerçekleştirirken, süreç hakkında size fikir verir. C # programından gelişen IL kodu , son C # kodunun gerçek çevirisidir . Burada kendiniz görebilirsiniz: https://dotnetfiddle.net/QaiLRz ('IL'yi görüntüle' bağlantısını tıklayın)
Şimdi, burada gözlemlediğiniz bir şey, işlem sırasında kodun daha karmaşık hale gelmesidir. Bunu gözlemlemenin en kolay yolu, aynı şeyi kabul etmek için gittikçe daha fazla koda ihtiyaç duymamızdır. Ayrıca iddia edebilir foreach
, for
, while
ve break
aslında kısa ellerdir goto
kısmen doğru olan.
IL'den Assembler'a
.NET JIT derleyicisi bir SSA derleyicisidir. Burada SSA formunun tüm ayrıntılarına ve optimize edici bir derleyicinin nasıl oluşturulacağına girmeyeceğim, çok fazla, ancak ne olacağı hakkında temel bir anlayış verebilir. Daha derin bir anlayış için, derleyicileri optimize etmeye okumaya başlamak en iyisidir (kısa bir giriş için bu kitabı beğendim: http://ssabook.gforge.inria.fr/latest/book.pdf ) ve LLVM (llvm.org) .
Her optimize edici derleyici, kodun kolay olduğu ve öngörülebilir kalıpları takip ettiği gerçeğine dayanır . FOR döngüleri durumunda, dalları analiz etmek için grafik teorisini kullanırız ve sonra da dallarımızdaki cycli gibi şeyleri optimize ederiz (örn. Geriye doğru dallar).
Ancak, şimdi döngülerimizi uygulamak için ileri şubelerimiz var. Tahmin edebileceğiniz gibi, bu aslında JIT'in düzelteceği ilk adımlardan biridir, şöyle:
int i=0; // for initialization
if (i >= array.Length) // for condition
{
goto endOfLoop;
}
startLoop:
var item = array[i];
// ...
goto endOfLoop; // break
// ...
++i; // for post-expression
if (i >= array.Length) // for condition
{
goto startLoop;
}
endOfLoop:
// ...
Gördüğünüz gibi, artık geri döngümüz var, bu da bizim küçük döngümüz. Burada hala kötü olan tek şey, break
açıklamamız nedeniyle sonuçta bulunduğumuz şubedir. Bazı durumlarda, bunu aynı şekilde hareket ettirebiliriz, ancak bazılarında kalmak için oradadır.
Peki derleyici bunu neden yapıyor? Döngüyü çözebilirsek, vektörleştirebiliriz. Sadece sabitlerin eklendiğini bile kanıtlayabiliriz, yani tüm döngümüz ince havaya kaybolabilir. Özetlemek gerekirse: paternleri öngörülebilir hale getirerek (dalları öngörülebilir hale getirerek), döngümüzde belirli koşulların geçerli olduğunu kanıtlayabiliriz, yani JIT optimizasyonu sırasında sihir yapabiliriz.
Bununla birlikte, dallar bu güzel öngörülebilir kalıpları kırma eğilimindedir, bu da optimize edicilerdir ve bu nedenle hoşnutsuz bir şeydir. Kır, devam et, git - hepsi bu öngörülebilir kalıpları kırmayı planlıyor - ve bu yüzden gerçekten 'hoş' değiller.
Bu noktada, bir basitliğin her yere yayılan foreach
bir grup goto
ifadeden daha öngörülebilir olduğunu da fark etmelisiniz . Optimize edici bakış açısından (1) okunabilirlik ve (2) açısından, hem daha iyi çözümdür.
Bahsetmeye değer başka bir şey de, derleyicileri değişkenlere kayıt atamak için optimize etmenin çok alakalı olmasıdır ( kayıt tahsisi adı verilen bir süreç ). Bildiğiniz gibi, CPU'nuzda sadece sınırlı sayıda kayıt vardır ve bunlar donanımınızdaki en hızlı bellek parçalarıdır. En içteki döngüde kodda kullanılan değişkenlerin bir kayıt atanması daha olasıdır, ancak döngünüzün dışındaki değişkenler daha az önemlidir (çünkü bu kod muhtemelen daha az isabetlidir).
Yardım, çok fazla karmaşıklık ... ne yapmalıyım?
Sonuç olarak, her zaman emrinizde olan dil yapılarını kullanmalısınız, bu da derleyiciniz için genellikle (dolaylı olarak) tahmin edilebilir kalıplar oluşturur. (: Özellikle mümkünse garip dalları kaçının break
, continue
, goto
ya da return
hiçbir şey ortasında).
Buradaki iyi haber şu ki bu öngörülebilir kalıplar hem insanlar için hem de okunması kolay (derleyiciler için).
Bu kalıplardan birine SESE denir ve Tek Girişli Tek Çıkış anlamına gelir.
Ve şimdi gerçek soruya geçiyoruz.
Bunun gibi bir şeyiniz olduğunu düşünün:
// a is a variable.
for (int i=0; i<100; ++i)
{
for (int j=0; j<100; ++j)
{
// ...
if (i*j > a)
{
// break everything
}
}
}
Bunu öngörülebilir bir desen haline getirmenin en kolay yolu, if
tamamen ortadan kaldırmaktır :
int i, j;
for (i=0; i<100 && i*j <= a; ++i)
{
for (j=0; j<100 && i*j <= a; ++j)
{
// ...
}
}
Diğer durumlarda yöntemi de 2 yönteme bölebilirsiniz:
// Outer loop in method 1:
for (i=0; i<100 && processInner(i); ++i)
{
}
private bool processInner(int i)
{
int j;
for (j=0; j<100 && i*j <= a; ++j)
{
// ...
}
return i*j<=a;
}
Geçici değişkenler? İyi, kötü ya da çirkin mi?
Hatta döngü içinden bir boolean döndürmeye karar verebilirsiniz (ama ben şahsen SESE formunu tercih ederim çünkü derleyici bunu görecek ve okumak daha temiz olduğunu düşünüyorum).
Bazı insanlar geçici bir değişken kullanmanın daha temiz olduğunu düşünüyor ve böyle bir çözüm öneriyor:
bool more = true;
for (int i=0; i<100; ++i)
{
for (int j=0; j<100; ++j)
{
// ...
if (i*j > a) { more = false; break; } // yuck.
// ...
}
if (!more) { break; } // yuck.
// ...
}
// ...
Ben şahsen bu yaklaşıma karşıyım. Kodun nasıl derlendiğine tekrar bakın. Şimdi bunun bu güzel, öngörülebilir kalıplarla ne yapacağını düşünün. Resmi al?
Doğru, heceleyelim. Ne olacak:
- Derleyici her şeyi dal olarak yazacaktır.
- Bir optimizasyon adımı olarak, derleyici
more
sadece kontrol akışında kullanılan garip değişkeni ortadan kaldırmak için veri akışı analizi yapacaktır .
- Başarılı olursa, değişken
more
programdan kaldırılır ve yalnızca dallar kalır. Bu dallar optimize edilecek, böylece iç döngüden sadece tek bir dal alacaksınız.
- Eğer başarısız olursa, değişken
more
en içteki döngüde kesinlikle kullanılır, bu nedenle derleyici onu optimize etmezse, bir kayda tahsis etme şansı yüksektir (değerli kayıt hafızasını tüketir).
Özetlemek gerekirse: derleyicinizdeki optimizer more
, yalnızca kontrol akışı için kullanıldığını anlamak için çok fazla sorun yaşayacaktır ve en iyi durumda senaryo , onu dış için tek bir dala çevirecektir. döngü.
Başka bir deyişle, en iyi senaryo, bunun eşdeğeri ile sonuçlanacağı yönündedir:
for (int i=0; i<100; ++i)
{
for (int j=0; j<100; ++j)
{
// ...
if (i*j > a) { goto exitLoop; } // perhaps add a comment
// ...
}
// ...
}
exitLoop:
// ...
Bununla ilgili kişisel düşüncem oldukça basit: eğer başından beri istediğimiz buysa, dünyayı hem derleyici hem de okunabilirlik için kolaylaştıralım ve hemen yazalım.
tl; dr:
Sonuç olarak:
- Mümkünse for döngüsü içinde basit bir koşul kullanın. Mümkün olduğunca üst düzey dil yapılarına bağlı kalın.
- Her şey başarısız olursa ve ya sol ediyorsanız
goto
veya bool more
eski tercih ederler.