Kuyruk çağrısı optimizasyonu birçok dilde ve derleyicide mevcuttur. Bu durumda, derleyici formun bir fonksiyonunu tanır:
int foo(n) {
...
return bar(n);
}
Burada dil, döndürülen sonucun başka bir fonksiyonun sonucudur ve yeni bir yığın çerçeveli bir fonksiyon çağrısını bir atlamaya dönüştürür.
Klasik faktöriyel yöntemin farkına varın:
int factorial(n) {
if(n == 0) return 1;
if(n == 1) return 1;
return n * factorial(n - 1);
}
olduğu değil çünkü dönüş gerekli denetim kuyruk çağrı optimizatable. ( Örnek kaynak kodu ve derlenmiş çıktı )
Bu kuyruk aramasını optimize edilebilir hale getirmek için,
int _fact(int n, int acc) {
if(n == 1) return acc;
return _fact(n - 1, acc * n);
}
int factorial(int n) {
if(n == 0) return 1;
return _fact(n, 1);
}
Bu kodu gcc -O2 -S fact.c
derlemek (derleyicideki optimizasyonu sağlamak için -O2 gereklidir, fakat -O3'ün daha fazla optimizasyonu ile bir insanın okuması zorlaşır ...)
_fact(int, int):
cmpl $1, %edi
movl %esi, %eax
je .L2
.L3:
imull %edi, %eax
subl $1, %edi
cmpl $1, %edi
jne .L3
.L2:
rep ret
( Örnek kaynak kodu ve derlenmiş çıktı )
Bir segment görebilir .L3
, jne
yerine göre call
(yeni bir yığın çerçeveye sahip olan bir alt yordam çağrı yapar).
Lütfen bunun C ile yapıldığına dikkat edin. Java'da kuyruk çağrısı optimizasyonu zordur ve JVM uygulamasına bağlıdır (ki, bunu yapmak için hiçbir şey görmedim, çünkü zor ve yığın çerçeveleri gerektiren gerekli Java güvenlik modelinin etkileri) - bu, TCO'nun önlediği şeydir) - tail-recursion + java ve tail-recursion + optimizasyonu , göz atmak için iyi etiket kümeleridir. Diğer JVM dillerinin kuyruk özyinelemesini daha iyi optimize edebildiğini görebilirsiniz (clojure'u deneyin ( arama sırasını optimize etmek için tekrarlama gerektirir ) veya ölçeklendirme).
Bahsedilen,
Bir şeyi doğru yazdığınızı bilmenin kesin bir neşesi var - yapılabilecek en ideal şekilde.
Şimdi biraz viski alacağım ve biraz Alman elektroniği yapacağım ...
"Özyinelemeli bir algoritmada yığın taşması önleme yöntemleri" genel sorusuna ...
Diğer bir yaklaşım ise özyinelemeli bir sayaç içermektir. Bu, kontrolünün dışındaki (ve zayıf kodlama) durumların neden olduğu sonsuz döngüleri tespit etmek için daha fazladır.
Özyineleme sayacı şeklini alır.
int foo(arg, counter) {
if(counter > RECURSION_MAX) { return -1; }
...
return foo(arg, counter + 1);
}
Her arama yaptığınızda, sayacı artırırsınız. Sayaç çok büyürse, hata yaparsınız (burada, sadece -1 dönüş, ancak diğer dillerde istisna atmayı tercih edebilirsiniz). Fikir, beklenenden çok daha derin ve muhtemelen sonsuz bir döngü olan bir özyineleme yaparken daha kötü şeylerin oluşmasını (bellek dışı hatalar) önlemektir.
Teoride buna ihtiyacın olmamalı. Uygulamada, küçük hatalardan ve kötü kodlama uygulamalarından oluşan bir bolluk yüzünden buna isabet eden kötü yazılmış bir kod gördüm (çok iş parçacıklı eşzamanlılık sorunları, başka bir iş parçacığını sonsuz özyinelemeli çağrılara götüren yöntemin dışında bir şey değiştiren sorunlar).
Doğru algoritmayı kullanın ve doğru problemi çözün. Özellikle Collatz Sanısı için, göründüğü sen bunu çözmeye çalıştıklarını xkcd şekilde:
Bir numaradan başlıyorsunuz ve bir ağaç geçişi yapıyorsunuz. Bu hızla çok geniş bir arama alanına yol açar. Doğru cevap için yineleme sayısını hesaplamak için yapılan hızlı bir işlem yaklaşık 500 adımda sonuçlanır. Bu küçük bir yığın çerçeveli özyinelemeyle ilgili bir sorun olmamalıdır.
Özyinelemeli çözümü bilmek kötü bir şey olmasa da, yinelemeli çözümün daha iyi olduğunu defalarca fark etmek gerekir . Özyinelemeli bir algoritmayı yinelemeli bir algoritmaya dönüştürmeye yaklaşmanın birkaç yolu, özyinelemeden yinelemeye geçmek için Yol Taşma Taşması'nda görülebilir .