.NET / C # neden kuyruk çağrısı özyinelemesini optimize etmiyor?


111

Hangi dillerin kuyruk özyinelemesini optimize ettiğiyle ilgili bu soruyu buldum . C # neden mümkün olduğunda kuyruk özyinelemesini optimize etmiyor?

Somut bir durum için, bu yöntem neden bir döngü halinde optimize edilmiyor ( önemliyse Visual Studio 2008 32 bit) ?:

private static void Foo(int i)
{
    if (i == 1000000)
        return;

    if (i % 100 == 0)
        Console.WriteLine(i);

    Foo(i+1);
}

Bugün Veri Yapıları üzerine, özyinelemeli işlevi ikiye ayıran preemptive(örneğin faktöryel algoritma) ve Non-preemptive(örneğin, ackermann'ın işlevi) bir kitap okuyordum . Yazar, bu çatallanmanın ardında uygun bir gerekçe göstermeden bahsettiğim sadece iki örnek verdi. Bu çatallanma, kuyruk ve kuyruk olmayan özyinelemeli işlevlerle aynı mı?
RBT

5
Jon skeet ve Scott Hanselman tarafından 2016'da youtu.be/H2KkiRbDZyc?t=3302
Daniel B

@RBT: Bunun farklı olduğunu düşünüyorum. Özyinelemeli çağrıların sayısını ifade eder. Kuyruk aramaları, kuyruk konumunda görünen aramalar hakkındadır, yani bir işlevin yaptığı son şey, sonucu doğrudan aranan uçtan döndürür.
JD

Yanıtlar:


84

JIT derlemesi, derleme aşamasını yapmak için çok fazla zaman harcamamak (dolayısıyla kısa ömürlü uygulamaları önemli ölçüde yavaşlatmak) ile uygulamayı uzun vadede standart bir önceden derleme ile rekabetçi tutmak için yeterli analiz yapmamak arasında zor bir dengeleme eylemidir. .

İlginçtir ki, NGen derleme adımları, optimizasyonlarında daha agresif olmayı hedeflemiyor. Bunun nedeni, davranışın makine kodundan JIT'in mi yoksa NGen'in mi sorumlu olduğuna bağlı olan hataların olmasını istemediklerinden şüpheleniyorum.

CLR kendisi destek kuyruk çağrı optimizasyonu, ama dile özgü derleyici alakalı üretmek bilmelidir işlem kodu ve JIT saygı istekli olmalıdır. F # 'ın fsc'si ilgili işlem kodlarını üretecektir (ancak basit bir özyineleme için her şeyi whiledoğrudan bir döngüye dönüştürebilir ). C # 's csc yapmaz.

Bkz bu blog yazısı (son JIT değişikliklerde belirtilen artık güncel oldukça olasılıkla) bazı detaylar için. CLR'nin 4.0 için x86, x64 ve ia64 için değişikliklere uyacağını unutmayın .


2
Ayrıca şu gönderiye bakın: social.msdn.microsoft.com/Forums/en-US/netfxtoolsdev/thread/… burada kuyruğun normal bir aramadan daha yavaş olduğunu keşfettim. Eep!
kaide

77

Bu Microsoft Connect geri bildirim gönderimi sorunuzu yanıtlamalıdır. Microsoft'tan resmi bir yanıt içeriyor, bu yüzden bunu takip etmenizi öneririm.

Önerin için teşekkürler. C # derleyicisinin geliştirilmesinde bir dizi noktada kuyruk çağrısı talimatlarını yayınlamayı düşündük. Bununla birlikte, bizi şimdiye kadar bundan kaçınmaya iten bazı ince sorunlar var: 1) CLR'de .tail talimatını kullanmanın aslında önemsiz olmayan bir genel maliyeti vardır (kuyruk çağrıları nihayetinde kuyruk aramalarının büyük ölçüde optimize edildiği işlevsel dil çalışma zamanı ortamları gibi daha az katı ortamlarda. 2) Kuyruk çağrıları yaymanın yasal olacağı birkaç gerçek C # yöntemi vardır (diğer diller daha fazla kuyruk özyinelemesine sahip kodlama modellerini teşvik eder, ve kuyruk çağrısı optimizasyonuna büyük ölçüde güvenen birçoğu, kuyruk özyinelemesinin miktarını artırmak için aslında genel yeniden yazma (Devam Eden Geçiş dönüşümleri gibi) yapar. 3) Kısmen 2) nedeniyle, C # yöntemlerinin başarılı olması gereken derin özyineleme nedeniyle taşması durumları oldukça nadirdir.

Bütün bunlar, buna bakmaya devam ediyoruz ve derleyicinin gelecekteki bir sürümünde .tail talimatlarını yayınlamanın mantıklı olduğu bazı kalıplar bulabiliriz.

O işaret edildiği gibi arada, bu kuyruk özyineleme dikkati çekiyor edilir x64 optimize.


3
Bunu da yararlı bulabilirsiniz: weblogs.asp.net/podwysocki/archive/2008/07/07/…
Noldorin

Sorun yok, yararlı bulduğunuza sevindim.
Noldorin

17
Alıntı yaptığınız için teşekkürler, çünkü şimdi bir 404!
Roman Starkov

3
Bağlantı artık düzeltildi.
luksan

15

C #, kuyruk çağrısı özyinelemesini optimize etmez çünkü F # bunun içindir!

C # derleyicisinin kuyruk çağrısı optimizasyonları gerçekleştirmesini engelleyen koşullarla ilgili biraz derinlik için şu makaleye bakın: JIT CLR kuyruk çağrısı koşulları .

C # ve F # arasında birlikte çalışabilirlik

C # ve F # birlikte çok iyi çalışır ve .NET Ortak Dil Çalışma Zamanı (CLR) bu birlikte çalışabilirlik göz önünde bulundurularak tasarlandığından, her dil, amacına ve amaçlarına özgü optimizasyonlarla tasarlanmıştır. C # kodundan F # kodunu çağırmanın ne kadar kolay olduğunu gösteren bir örnek için, bkz. C # kodundan F # kodunu çağırma ; F # kodundan C # işlevlerini çağırmanın bir örneği için, bkz . F # 'dan C # işlevlerini çağırma .

Temsilci birlikte çalışabilirliği için şu makaleye bakın: F #, C # ve Visual Basic arasında birlikte çalışabilirlik temsilcisi seçin .

C # ve F # arasındaki teorik ve pratik farklar

C # ve F # arasındaki kuyruk çağrısı özyinelemesinin bazı farklılıklarını ele alan ve tasarım farklılıklarını açıklayan bir makale: C # ve F # 'da Kuyruk Çağrısı İşlem Kodu Oluşturma .

C #, F # ve C ++ \ CLI'de bazı örnekler içeren bir makale: C #, F # ve C ++ \ CLI'de Kuyruk Özyinelemede Maceralar

Temel teorik fark, C # 'nin döngülerle tasarlanması, F #' nin ise Lambda hesabı ilkelerine göre tasarlanmasıdır. Lambda hesabı ilkeleri üzerine çok iyi bir kitap için, Abelson, Sussman ve Sussman'ın yazdığı şu ücretsiz kitap: Bilgisayar Programlarının Yapısı ve Yorumlanması kitabına bakın .

F # 'da kuyruk aramaları hakkında çok iyi bir giriş makalesi için, bu makaleye bakın: F #' da Kuyruk Çağrılarına Ayrıntılı Giriş . Son olarak, burada kuyruk olmayan özyineleme ile kuyruk çağrısı özyinelemesi (F #) arasındaki farkı kapsayan bir makale var: Kuyruk özyinelemeye karşı kuyruk olmayan özyineleme F diyezde .


8

Geçenlerde 64 bit için C # derleyicisinin kuyruk özyinelemesini optimize ettiği söylendi.

C # da bunu uygular. Her zaman uygulanmamasının nedeni, kuyruk özyinelemeyi uygulamak için kullanılan kuralların çok katı olmasıdır.


8
X64 jitter bunu yapar, ancak C # derleyicisi yapmaz
Mark Sowul

bilgi için teşekkürler. Bu daha önce düşündüğümden farklı beyaz.
Alexandre Brisebois

3
Sadece bu iki yorumu açıklığa kavuşturmak için, C # hiçbir zaman CIL 'kuyruk' işlem kodunu yaymaz ve bunun 2017'de hala doğru olduğuna inanıyorum. Bununla birlikte, tüm diller için bu işlem kodu her zaman yalnızca ilgili titreşimler (x86, x64) anlamında tavsiye niteliğindedir. ), çeşitli koşullar karşılanmazsa sessizce yok sayar (iyi, olası yığın taşması dışında hata yoktur ). Bu, neden "kuyruk" u "ret" ile takip etmek zorunda kaldığınızı açıklıyor - bu durum için. Bu arada, titremeler, yine uygun görüldüğü şekilde ve .NET dilinden bağımsız olarak, CIL'de 'kuyruk' öneki olmadığında optimizasyonu uygulamakta özgürdür.
Glenn Slayden

3

C # (veya Java) 'da kuyruk özyinelemeli işlevler için trambolin tekniğini kullanabilirsiniz . Bununla birlikte, daha iyi çözüm (yalnızca yığın kullanımını önemsiyorsanız), aynı özyinelemeli işlevin parçalarını sarmak ve işlevi okunabilir tutarken yinelemeli hale getirmek için bu küçük yardımcı yöntemi kullanmaktır .


Trambolinler istilacıdır (çağrı kuralına genel bir değişikliktir), uygun kuyruk çağrısı eliminasyonundan ~ 10 kat daha yavaştır ve tüm yığın izleme bilgilerini
JD

1

Belirtilen diğer cevaplar gibi, CLR kuyruk arama optimizasyonunu destekliyor ve tarihsel olarak ilerici iyileştirmeler altında görünüyor. Ancak C # ile desteklenmesinin Proposalgit deposunda C # programlama dilinin tasarımı için açık bir sorunu var Destek kuyruk özyinelemesi # 2544 .

Orada bazı yararlı ayrıntılar ve bilgiler bulabilirsiniz. Örneğin @jaykrell bahsedildi

Bildiklerimi vereyim.

Bazen arka arkaya çağrı, performans açısından bir kazan-kazan demektir. CPU'dan tasarruf edebilir. jmp, call / ret'den daha ucuzdur. Yığını kaydedebilir. Daha az yığına dokunmak daha iyi bir konum sağlar.

Ara sıra bazen performans kaybıdır, yığın kazanır. CLR, aranan uca, arayanın aldığından daha fazla parametreyi iletmek için karmaşık bir mekanizmaya sahiptir. Parametreler için özellikle daha fazla yığın alanı demek istiyorum. Bu yavaş. Ancak yığını korur. Bunu sadece kuyrukla yapacak. önek.

Arayan parametreleri, aranan parametrelerden yığın büyüklüğünde ise, genellikle oldukça kolay bir kazan-kazan dönüşümüdür. Parametre konumunun yönetilen konumdan tamsayı / kayan sayıya değişmesi ve hassas StackMap'ler ve benzerlerinin oluşturulması gibi faktörler olabilir.

Şimdi, sabit / küçük yığınla keyfi olarak büyük verileri işleyebilmek amacıyla kuyruk çağrısının ortadan kaldırılmasını gerektiren algoritmaların başka bir açısı var. Bu performansla ilgili değil, koşma yeteneği ile ilgili.

Ayrıca şunu da belirteyim (ekstra bilgi olarak), İsim System.Linq.Expressionsalanındaki ifade sınıflarını kullanarak derlenmiş bir lambda oluşturduğumuzda, açıklamasında açıklandığı gibi 'tailCall' adında bir argüman vardır.

Oluşturulan ifadeyi derlerken kuyruk çağrısı optimizasyonunun uygulanıp uygulanmayacağını gösteren bir bool.

Henüz denemedim ve sorunuzla ilgili olarak nasıl yardımcı olacağından emin değilim, ancak Muhtemelen birisi deneyebilir ve bazı senaryolarda faydalı olabilir:


var myFuncExpression = System.Linq.Expressions.Expression.Lambda<Func<  >>(body:  , tailCall: true, parameters:  );

var myFunc =  myFuncExpression.Compile();
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.