Bir işlevde erken dönüşün etkinliği


97

Bu, deneyimsiz bir programcı olarak sıklıkla karşılaştığım ve özellikle optimize etmeye çalıştığım iddialı, hız yoğun bir projem için merak ettiğim bir durum. Başlıca C benzeri diller (C, objC, C ++, Java, C #, vb.) Ve bunların her zamanki derleyicileri için bu iki işlev de aynı derecede verimli çalışacak mı? Derlenen kodda herhangi bir fark var mı?

void foo1(bool flag)
{
    if (flag)
    {
        //Do stuff
        return;
    }

    //Do different stuff
}

void foo2(bool flag)
{
    if (flag)
    {
        //Do stuff
    }
    else
    {
        //Do different stuff
    }
}

Temel olarak, erken başlarken breakveya returnerken gelirken doğrudan bir verimlilik bonusu / cezası var mı? Stackframe nasıl dahil edilir? Optimize edilmiş özel durumlar var mı? Bunu önemli ölçüde etkileyebilecek herhangi bir faktör (satır içi veya "İşler yap" ın boyutu gibi) var mı?

Her zaman küçük optimizasyonlara göre daha iyi okunabilirliğin savunucusuyum (foo1'i parametre doğrulamada çok görüyorum), ancak bu o kadar sık ​​ortaya çıkıyor ki tüm endişeleri bir kez ve tümüyle bir kenara bırakmak istiyorum.

Ve erken optimizasyonun tuzaklarının farkındayım ... ugh, bunlar acı verici anılar.

DÜZENLEME: Bir cevabı kabul ettim, ancak EJP'nin cevabı oldukça kısa ve öz bir returnşekilde a kullanımının neden neredeyse ihmal edilebilir olduğunu açıklıyor (montajda, returnfonksiyonun sonuna kadar son derece hızlı bir 'dallanma' yaratıyor. Dal, PC sicilini değiştiriyor ve Her iki yüzünden de önbellek ve boru hattını oldukça ufacık budur.) özellikle bu durum için etkileyebilir, bu anlamıyla hiç fark etmez if/elseve returnfonksiyonun sonuna aynı dalı oluşturmak.


22
Bu tür şeylerin performans üzerinde kayda değer bir etkisi olacağını düşünmüyorum. Sadece küçük bir test yazın ve kendinizi görün. Imo, ilk varyant daha iyi, çünkü gereksiz yuvalama olmaz ve bu da hazırlığı artırır
SirVaulterScoff

10
@SirVaulterScott, iki durum bir şekilde simetrik olmadıkça, bu durumda simetriyi aynı girinti seviyesine koyarak ortaya çıkarmak istersiniz.
luqui

3
SirVaulterScoff: Gereksiz yuvalanmayı azaltmak için +1
fjdumont

11
Okunabilirlik >>> Mikro optimizasyonlar. Bunu koruyacak olan yazılım için hangisi daha mantıklıysa onu yapın. Bir makine kodu seviyesinde, bu iki yapı oldukça aptal bir derleyiciye bile beslendiğinde aynıdır. Optimize edici bir derleyici, ikisi arasındaki herhangi bir hız avantajını siler.
SplinterReality

12
Bunun gibi şeyler için endişelenerek "yoğun hız gerektiren" projenizi optimize etmeyin. Gerçekte nerede yavaş olduğunu bulmak için uygulamanızın profilini oluşturun - çalışmasını tamamladığınızda gerçekten çok yavaşsa. Neyin onu yavaşlattığını neredeyse kesinlikle tahmin edemezsiniz.
blueshift

Yanıtlar:


92

Hiç bir fark yok:

=====> cat test_return.cpp
extern void something();
extern void something2();

void test(bool b)
{
    if(b)
    {
        something();
    }
    else
        something2();
}
=====> cat test_return2.cpp
extern void something();
extern void something2();

void test(bool b)
{
    if(b)
    {
        something();
        return;
    }
    something2();
}
=====> rm -f test_return.s test_return2.s
=====> g++ -S test_return.cpp 
=====> g++ -S test_return2.cpp 
=====> diff test_return.s test_return2.s
=====> rm -f test_return.s test_return2.s
=====> clang++ -S test_return.cpp 
=====> clang++ -S test_return2.cpp 
=====> diff test_return.s test_return2.s
=====> 

İki derleyicide optimizasyon yapılmasa bile üretilen kodda hiçbir fark olmaması anlamına gelir


59
Ya da daha iyisi: iki sürüm için aynı kodu üreten belirli bir derleyicinin en azından bir sürümü vardır.
UncleZeiv

11
@UncleZeiv - tüm derleyiciler olmasa da çoğu kaynağı bir yürütme akış grafiği modeline çevirecektir. Bu iki örnek için anlamlı şekilde farklı akış grafikleri verecek mantıklı bir uygulama hayal etmek zor . Görebileceğiniz tek fark hakkında, iki farklı bir şeyler yapmanın değişmesi ve hatta bu, dal tahminini optimize etmek için birçok uygulamada veya platformun tercih edilen sıralamayı belirlediği başka bir sorun için geri alınabilir.
Steve314

6
@ Steve314, elbette, sadece
nitpicking

@UncleZeiv: clang üzerinde de test edildi ve aynı sonuç
Dani

Ben anlamadım Her something()zaman idam edilecek gibi görünüyor . Orijinal soruda, OP'nin var Do stuffve Do diffferent stuffbayrağa bağlı olarak. Üretilen kodun aynı olacağından emin değilim.
Luc M

65

Kısa cevap, fark yok. Kendinize bir iyilik yapın ve bunun için endişelenmeyi bırakın. Optimize edici derleyici neredeyse her zaman sizden daha akıllıdır.

Okunabilirlik ve sürdürülebilirliğe odaklanın.

Ne olacağını görmek istiyorsanız, bunları optimizasyonlarla oluşturun ve assembler çıktısına bakın.


8
@Philip: Herkese de bir iyilik yapın ve bunun için endişelenmeyi bırakın. Yazdığınız kod başkaları tarafından da okunacak ve korunacaktır (ve başkaları tarafından asla okunmayacak şekilde yazsanız bile, yazdığınız diğer kodları etkileyecek ve başkaları tarafından okunacak alışkanlıklar geliştirirsiniz). Mümkün olduğunca kolay anlaşılması için her zaman kod yazın.
hlovdal

8
Doktorlar sizden daha akıllı değil !!! Sadece etkinin nerede önemli olmadığına karar vermede daha hızlıdırlar. Gerçekten önemli olduğu yerde, kesinlikle bazı deneyimlerle derleyiciden daha iyi optimize edeceksiniz.
johannes

10
@johannes Katılmama izin ver. Derleyici, algoritmanızı daha iyisi için değiştirmez, ancak maksimum boru hattı verimliliğine ulaşmak için talimatları yeniden sıralamakta ve deneyimli bir programcının bile karar veremeyeceği döngüler için (fisyon, füzyon vb.) CPU mimarisi hakkında derinlemesine bilgi sahibi olmadıkça daha iyi olan şey.
fortran

3
@johannes - bu soru için öyle olduğunu varsayabilirsiniz. Ayrıca, genel olarak, birkaç özel durumda zaman zaman derleyiciden daha iyi optimize edebilirsiniz, ancak bu bugünlerde oldukça fazla uzman bilgisi gerektirir - normal durum, optimizasyon aracının aklınıza gelebilecek çoğu optimizasyonu uygular ve bunu yapar. sistematik olarak, sadece birkaç özel durumda değil. Bu soruyu WRT, derleyici muhtemelen her iki form için de tam olarak aynı yürütme akış grafiğini oluşturacaktır. Daha iyi bir algoritma seçmek insan işidir, ancak kod düzeyinde optimizasyon neredeyse her zaman zaman kaybıdır.
Steve314

4
Buna katılıyorum ve katılmıyorum. Derleyicinin bir şeyin başka bir şeye eşdeğer olduğunu bilemediği durumlar vardır. Bunu yapmanın, genellikle Uneed dalların gerçekten incitebileceğinden x = <some number>çok daha hızlı olduğunu biliyor muydunuz? if(<would've changed>) x = <some number>Öte yandan, bu aşırı yoğun bir operasyonun ana döngüsünün içinde olmadığı sürece, ben de endişelenmem.
user606723

28

İlginç cevaplar: Hepsine (şimdiye kadar) katılmama rağmen, bu sorunun şimdiye kadar tamamen göz ardı edilen olası çağrışımları var.

Yukarıdaki basit örnek kaynak tahsisi ile genişletilirse ve ardından kaynakların serbest bırakılmasıyla sonuçlanabilecek bir hata kontrolü yapılırsa, resim değişebilir.

Yeni başlayanların alabileceği saf yaklaşımı düşünün :

int func(..some parameters...) {
  res_a a = allocate_resource_a();
  if (!a) {
    return 1;
  }
  res_b b = allocate_resource_b();
  if (!b) {
    free_resource_a(a);
    return 2;
  }
  res_c c = allocate_resource_c();
  if (!c) {
    free_resource_b(b);
    free_resource_a(a);
    return 3;
  }

  do_work();

  free_resource_c(c);
  free_resource_b(b);
  free_resource_a(a);

  return 0;
}

Yukarıdakiler, erken dönme tarzının aşırı bir versiyonunu temsil eder. Karmaşıklığı arttığında, kodun zaman içinde nasıl çok tekrarlayıcı ve bakımsız hale geldiğine dikkat edin. Günümüzde insanlar bunları yakalamak için istisna işlemeyi kullanabilir .

int func(..some parameters...) {
  res_a a;
  res_b b;
  res_c c;

  try {
    a = allocate_resource_a(); # throws ExceptionResA
    b = allocate_resource_b(); # throws ExceptionResB
    c = allocate_resource_c(); # throws ExceptionResC
    do_work();
  }  
  catch (ExceptionBase e) {
    # Could use type of e here to distinguish and
    # use different catch phrases here
    # class ExceptionBase must be base class of ExceptionResA/B/C
    if (c) free_resource_c(c);
    if (b) free_resource_b(b);
    if (a) free_resource_a(a);
    throw e
  }
  return 0;
}

Philip, aşağıdaki örneğe baktıktan sonra, yukarıdaki yakalama bloğunun içinde kesintisiz bir anahtar / kasa kullanmayı önerdi . Biri (typeof (e)) değiştirebilir ve sonra free_resourcex()çağrıların arasından geçebilir, ancak bu önemsiz değildir ve tasarımın dikkate alınması gerekir . Kesintisiz bir anahtarın / kasanın, aşağıdaki zincirleme etiketlere sahip olana tamamen benzediğini unutmayın ...

Mark B belirttiği gibi, C ++ bunun takip etmek iyi tarzı olarak kabul edilir Kaynak Edinme Başlatma olduğu ilkesi, RAII Short. Kavramın özü, kaynakları elde etmek için nesne somutlaştırmayı kullanmaktır. Nesneler kapsam dışına çıkar çıkmaz kaynaklar otomatik olarak serbest bırakılır ve yıkıcıları çağrılır. Birbirine bağlı kaynaklar için, doğru ayırma sırasının sağlanması ve tüm yıkıcılar için gerekli verilerin mevcut olacağı şekilde nesne türlerinin tasarlanması için özel dikkat gösterilmelidir.

Veya istisnai günlerde şunları yapabilir:

int func(..some parameters...) {
  res_a a = allocate_resource_a();
  res_b b = allocate_resource_b();
  res_c c = allocate_resource_c();
  if (a && b && c) {   
    do_work();
  }  
  if (c) free_resource_c(c);
  if (b) free_resource_b(b);
  if (a) free_resource_a(a);

  return 0;
}

Ancak bu aşırı basitleştirilmiş örneğin birkaç dezavantajı vardır: Yalnızca tahsis edilen kaynaklar birbirine bağlı değilse kullanılabilir (örneğin, bellek ayırmak için kullanılamaz, sonra bir dosya tanıtıcısı açar, ardından tutamacından belleğe veri okur) ) ve dönüş değerleri olarak bireysel, ayırt edilebilir hata kodları sağlamaz.

Linus Torvalds , kodu hızlı (!), Kompakt ve kolayca okunabilir ve genişletilebilir tutmak için, meşhur goto'yu kesinlikle mantıklı bir şekilde kullanarak bile, kaynaklarla ilgilenen farklı bir çekirdek kodu stili uyguladı :

int func(..some parameters...) {
  res_a a;
  res_b b;
  res_c c;

  a = allocate_resource_a() || goto error_a;
  b = allocate_resource_b() || goto error_b;
  c = allocate_resource_c() || goto error_c;

  do_work();

error_c:
  free_resource_c(c);
error_b:
  free_resource_b(b);
error_a:
  free_resource_a(a);

  return 0;
}

Çekirdek posta listeleri hakkındaki tartışmanın özü, goto deyimine göre "tercih edilen" çoğu dil özelliğinin büyük, ağaç benzeri if / else, istisna işleyicileri, döngü / break / continue ifadeleri vb. Gibi örtük gotos olmasıdır. Ve yukarıdaki örnekteki goto'lar, sadece küçük bir mesafeden atladıkları, açık etiketlere sahip oldukları ve hata koşullarını takip etmek için diğer karmaşanın kodunu serbest bıraktıkları için iyi kabul edilir. Bu soru ayrıca burada stackoverflow'da tartışılmıştır .

Ancak son örnekte eksik olan şey, bir hata kodu döndürmenin güzel bir yoludur. result_code++Her free_resource_x()aramadan sonra bir eklemeyi ve bu kodu döndürmeyi düşünüyordum, ancak bu, yukarıdaki kodlama stilinin bazı hız kazanımlarını dengeliyor. Ve başarı durumunda 0 döndürmek zordur. Belki ben hayal gücümden yoksun ;-)

Yani, evet, erken getirileri kodlama konusunda büyük bir fark olduğunu düşünüyorum. Ancak, derleyici için yeniden yapılandırmanın ve optimize etmenin daha zor veya imkansız olan yalnızca daha karmaşık kodda açıkça görüldüğünü düşünüyorum. Bu genellikle kaynak tahsisi devreye girdiğinde ortaya çıkar.


1
Vay canına, gerçekten ilginç. Saf yaklaşımın sürdürülemezliğini kesinlikle takdir edebilirim. Yine de bu özel durumda istisna işleme nasıl gelişebilir? Bir gibi catchbir kırılma daha az içeren switchhata kodu deyimi?
Philip Guin

@Philip Temel istisna işleme örneği eklendi. Yalnızca goto'nun düşme olasılığı olduğunu unutmayın. Önerilen anahtarınız (typeof (e)) yardımcı olacaktır, ancak önemsiz değildir ve tasarım üzerinde düşünülmesi gerekir .
Kesintisiz

+1 bu, C / C ++ (veya hafızanın manuel olarak boşaltılmasını gerektiren herhangi bir dil) için doğru cevaptır. Şahsen, çok etiketli versiyonu sevmiyorum. Önceki şirketimde her zaman "goto fin" idi (bir Fransız şirketiydi). Fin'de herhangi bir hafızayı ayırırdık ve kod incelemesini geçebilecek tek goto kullanımı buydu.
Kip

1
C ++ 'da bu yaklaşımlardan hiçbirini yapmayacağınızı, ancak kaynakların düzgün bir şekilde temizlendiğinden emin olmak için RAII kullanacağınızı unutmayın.
Mark B

12

Bu pek bir cevap olmasa da, bir üretim derleyicisi optimizasyonda sizden çok daha iyi olacaktır. Okunabilirliği ve sürdürülebilirliği bu tür optimizasyonlara tercih ederim.


9

Bu konuda net olmak gerekirse return, yöntemin sonuna kadar, bir RETtalimatın veya ne olursa olsun olacağı bir dalda derlenecektir . Bunu dışarıda bırakırsanız, bloğun önündeki sonu, bloğun sonuna elsekadar bir dalda derlenecektir else. Dolayısıyla, bu özel durumda bunun hiçbir fark yaratmadığını görebilirsiniz.


Anladım. Aslında bunun sorumu oldukça kısaca yanıtladığını düşünüyorum; Sanırım kelimenin tam anlamıyla sadece bir yazmaç eklemesi, ki bu oldukça ihmal edilebilir (belki sistem programlaması yapmıyorsanız ve o zaman bile ...) Bundan onurlu bir şekilde bahsedeceğim.
Philip Guin

@Philip ne kayıt ek? Yolda hiçbir ekstra talimat yok.
Marquis of Lorne

Her ikisinin de kayıt ekleri olacaktır. Hepsi bir montaj şubesi, değil mi? Program sayacına bir ek mi? Burada yanılıyor olabilirim.
Philip Guin

1
@Philip Hayır, bir montaj şubesi bir montaj şubesidir. Elbette PC'yi etkiliyor ama tamamen yeniden yükleyerek olabilir ve aynı zamanda işlemcide boru hattı, önbellek vb. Gibi yan etkileri de vardır.
Marquis of Lorne

4

Özel derleyiciniz ve sisteminiz için derlenmiş kodda bir fark olup olmadığını gerçekten bilmek istiyorsanız, derlemeyi kendiniz derlemeniz ve incelemeniz gerekir.

Bununla birlikte, büyük şemada, derleyicinin sizin ince ayarlarınızdan daha iyi optimize edebileceği neredeyse kesindir ve yapamasa bile, programınızın performansı için gerçekten önemli olması pek olası değildir.

Bunun yerine, kodu insanların okuyup sürdürmesi için en net şekilde yazın ve derleyicinin en iyi yaptığı şeyi yapmasına izin verin: Kaynağınızdan yapabileceği en iyi derlemeyi oluşturun.


4

Örneğinizde, geri dönüş dikkat çekicidir. Geri dönüş, farklı şeyler meydana gelen // yukarıda / aşağıda bir veya iki sayfa olduğunda hata ayıklayan kişiye ne olur? Daha fazla kod olduğunda bulmak / görmek çok daha zordur.

void foo1(bool flag)
{
    if (flag)
    {
        //Do stuff
        return;
    }

    //Do different stuff
}

void foo2(bool flag)
{
    if (flag)
    {
        //Do stuff
    }
    else
    {
        //Do different stuff
    }
}

Elbette, bir işlev bir (hatta iki) sayfadan uzun olmamalıdır. Ancak hata ayıklama yönü henüz diğer yanıtların hiçbirinde ele alınmadı. Alınan nokta!
cfi

3

Blueshift'e kesinlikle katılıyorum: önce okunabilirlik ve sürdürülebilirlik !. Ama gerçekten endişeliyseniz (veya sadece derleyicinizin ne yaptığını öğrenmek istiyorsanız, ki bu uzun vadede kesinlikle iyi bir fikirdir), kendinize bakmalısınız.

Bu, bir derleyicinin kullanılması veya düşük seviyeli derleyici çıktısına (örneğin, montaj dili) bakmak anlamına gelir. C # veya herhangi bir .Net dilinde, burada belgelenen araçlar size ihtiyacınız olanı verecektir.

Ancak sizin de gözlemlediğiniz gibi, bu muhtemelen erken optimizasyondur.


1

Gönderen Çevik Yazılım El Sanatları isimli bir El Kitabı: Temiz Kanunu

Bayrak argümanları çirkin. Bir booleanı bir işleve geçirmek gerçekten korkunç bir uygulamadır. Bu işlevin birden fazla şey yaptığını yüksek sesle ilan ederek, yöntemin imzasını derhal karmaşıklaştırır. Bayrak doğruysa bir şey, yanlışsa başka bir şey yapar!

foo(true);

kodda okuyucunun işleve gitmesini ve foo (boole bayrağı) okumak için zaman harcamasını sağlar.

Daha iyi yapılandırılmış kod tabanı, size kodu optimize etmek için daha iyi fırsat verecektir.


Bunu sadece örnek olarak kullanıyorum. İşleve aktarılan şey bir int, çift, bir sınıf olabilir, adını siz koyun, bu aslında sorunun merkezinde değildir.
Philip Guin

Sorduğunuz soru, işlevinizin içinde bir geçiş yapmakla ilgili, çoğu durumda bu bir kod kokusu. Pek çok yoldan elde edilebilir ve okuyucunun tüm işlevi okuması gerekmez, diyelim ki foo (28) ne anlama geliyor?
Yuan

0

Bir düşünce ekolü (şu anda onu öneren egghead'i hatırlayamıyorum), kodun okunmasını ve hata ayıklamasını kolaylaştırmak için tüm işlevlerin yapısal bir bakış açısından yalnızca bir dönüş noktasına sahip olması gerektiğidir. Sanırım bu daha çok dini tartışmaları programlamak için.

Bu kuralı ihlal eden bir fonksiyonun ne zaman ve nasıl çıkacağını kontrol etmek isteyebileceğiniz teknik bir neden, gerçek zamanlı uygulamaları kodladığınızda ve fonksiyon üzerinden tüm kontrol yollarının tamamlanması için aynı sayıda saat döngüsü aldığından emin olmak istemenizdir.


Uh, temizlemekle ilgisi olduğunu düşündüm (özellikle C ile kodlarken).
Thomas Eding

hayır, bir yöntemi nerede bırakırsanız bırakın, geri döndüğünüz sürece yığın aşağı doğru itilir ("temizlenen" her şey budur).
MartyTPS

-4

Bu soruyu gündeme getirdiğine sevindim. Şubeleri her zaman erken dönüş için kullanmalısınız. Neden orada duralım? Mümkünse tüm işlevlerinizi bir araya getirin (en azından yapabildiğiniz kadar). Özyineleme yoksa bu yapılabilir. Sonunda, büyük bir ana işleve sahip olacaksınız, ancak bu tür şeyler için ihtiyacınız olan / istediğiniz şey budur. Daha sonra tanımlayıcılarınızı olabildiğince kısa olacak şekilde yeniden adlandırın. Bu şekilde kodunuz çalıştırıldığında, isimleri okumak için daha az zaman harcanır. Sonraki yap ...


3
Şaka yaptığını söyleyebilirim, ama korkutucu olan şey, bazı insanların senin tavsiyeni ciddiye alması!
Daniel Pryden

Daniel ile aynı fikirde. Sinizmi sevdiğim kadarıyla - teknik dokümantasyonda, teknik incelemelerde ve SO gibi soru-cevap sitelerinde kullanılmamalıdır.
cfi

1
Alaycı bir cevap için -1, yeni başlayanlar tarafından mutlaka tanınmaz.
Johan Bezem
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.