"JVM kuyruk arama optimizasyonunu desteklemediğinden, çok sayıda patlayan yığın öngörüyorum"
Bunu söyleyen herkes (1) kuyruk çağrısı optimizasyonunu veya (2) JVM'yi veya (3) her ikisini de anlamıyor.
Wikipedia'dan kuyruk çağrılarının tanımıyla başlayacağım (Wikipedia'yı sevmiyorsanız, işte bir alternatif ):
Bilgisayar biliminde, kuyruk çağrısı, son eylemi olarak başka bir prosedür içinde gerçekleşen alt program çağrısıdır; daha sonra çağıran prosedür tarafından derhal döndürülen bir dönüş değeri üretebilir
Aşağıdaki kodda, çağrının bar()
kuyruğu foo()
:
private void foo() {
// do something
bar()
}
Kuyruk çağrısı optimizasyonu , kuyruk çağrısını gören dil uygulaması normal yöntem çağırma (yığın çerçevesi oluşturur) kullanmadığında, bunun yerine bir şube oluşturduğunda gerçekleşir. Bu bir optimizasyondur, çünkü yığın çerçevesi bellek gerektirir ve bilgileri çerçeveye (dönüş adresi gibi) çerçeveye itmek için CPU döngüleri gerektirir ve çağrı / dönüş çiftinin koşulsuz atlamadan daha fazla CPU döngüsü gerektirdiği varsayılır.
TCO genellikle özyinelemeye uygulanır, ancak tek kullanımı bu değildir. Tüm özyinelemeler için de geçerli değildir. Bir faktöriyeli hesaplamak için basit özyinelemeli kod, kuyruk çağrısı için optimize edilemez, çünkü işlevde gerçekleşen son şey bir çarpma işlemidir.
public static int fact(int n) {
if (n <= 1) return 1;
else return n * fact(n - 1);
}
Kuyruk çağrısı optimizasyonunu uygulamak için iki şeye ihtiyacınız vardır:
- Altyordam çağrılarına ek olarak dallanmayı destekleyen bir platform.
- Kuyruk çağrısı optimizasyonunun mümkün olup olmadığını belirleyebilen statik bir analizör.
Bu kadar. Başka bir yerde belirttiğim gibi, JVM (diğer Turing-complete mimarisi gibi) bir goto'ya sahip. Koşulsuz bir gitme olur , ancak işlevsellik bir koşullu dal kullanılarak kolayca uygulanabilir.
Statik analiz parçası zor olan şeydir. Tek bir işlev içinde sorun değil. Örneğin, işte bir özyinelemeli Scala işlevi List
:
def sum(acc:Int, list:List[Int]) : Int = {
if (list.isEmpty) acc
else sum(acc + list.head, list.tail)
}
Bu işlev aşağıdaki bayt koduna dönüşür:
public int sum(int, scala.collection.immutable.List);
Code:
0: aload_2
1: invokevirtual #63; //Method scala/collection/immutable/List.isEmpty:()Z
4: ifeq 9
7: iload_1
8: ireturn
9: iload_1
10: aload_2
11: invokevirtual #67; //Method scala/collection/immutable/List.head:()Ljava/lang/Object;
14: invokestatic #73; //Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
17: iadd
18: aload_2
19: invokevirtual #76; //Method scala/collection/immutable/List.tail:()Ljava/lang/Object;
22: checkcast #59; //class scala/collection/immutable/List
25: astore_2
26: istore_1
27: goto 0
Not goto 0
sonunda. Karşılaştırıldığında, eşdeğer bir Java işlevi ( Iterator
bir Scala listesini baş ve kuyruğa bölme davranışını taklit etmek için bir kullanması gerekir ) aşağıdaki bayt koduna dönüşür. Son iki işlemin şimdi bir çağırma olduğunu ve ardından bu özyinelemeli çağırma tarafından üretilen değerin açık bir şekilde geri döndüğünü unutmayın.
public static int sum(int, java.util.Iterator);
Code:
0: aload_1
1: invokeinterface #64, 1; //InterfaceMethod java/util/Iterator.hasNext:()Z
6: ifne 11
9: iload_0
10: ireturn
11: iload_0
12: aload_1
13: invokeinterface #70, 1; //InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
18: checkcast #25; //class java/lang/Integer
21: invokevirtual #74; //Method java/lang/Integer.intValue:()I
24: iadd
25: aload_1
26: invokestatic #43; //Method sum:(ILjava/util/Iterator;)I
29: ireturn
Tek bir fonksiyonun Kuyruk çağrı optimizasyonu önemsiz şudur: yerine böylece derleyici, aramanın sonucunu kullanan kod bulunmadığından olduğunu görebilirsiniz çağırma işlemi bir ile goto
.
Hayatın zorlaştığı yer birden fazla yönteminiz varsa. JVM'nin dallanma talimatları, 80x86 gibi genel amaçlı bir işlemcinin aksine, tek bir yöntemle sınırlıdır. Özel yöntemleriniz varsa hala nispeten basittir: derleyici bu yöntemleri uygun şekilde satır içi yapmakta serbesttir, bu nedenle kuyruk çağrılarını optimize edebilir (bunun nasıl çalıştığını merak ediyorsanız, switch
davranışı kontrol etmek için bir ortak yöntem kullanmayı düşünün ). Bu tekniği aynı sınıftaki birden çok genel yönteme bile genişletebilirsiniz: derleyici yöntem gövdelerini sıralar, genel köprü yöntemleri sağlar ve dahili çağrılar atlamalara dönüşür.
Ancak, özellikle arabirimler ve sınıf yükleyiciler ışığında, farklı sınıflardaki genel yöntemleri düşündüğünüzde bu model bozulur. Kaynak düzeyinde derleyici, kuyruk çağrısı optimizasyonlarını uygulamak için yeterli bilgiye sahip değildir. Ancak, aksine bunun olmadığını aslında gerçekleştirir bilmiyorum "çıplak metal" uygulamaları, * JVM ((en azından, eski Güneş derleyici yapar). Hotspot derleyici şeklinde Bunu yapmak için bilgi var kuyruk çağrı optimizasyonları ve şüpheli değil, ama olabilir .
Bu da beni sorunuzun ikinci kısmına getiriyor, hangisini "umursalıyız?"
Açıkça görüldüğü gibi, diliniz yinelemenin tek ilkesi olarak özyinelemeyi kullanıyorsa, önemsiyorsunuz. Ancak, bu özelliğe ihtiyaç duyan diller onu uygulayabilir; tek sorun, söz konusu dil için bir derleyicinin rasgele bir Java sınıfı tarafından çağrılabilen ve çağrılabilen bir sınıf üretip üretemeyeceğidir.
Bu davanın dışında, ilgisiz olduğunu söyleyerek aşağı oy davet edeceğim. Gördüğüm özyinelemeli kodların çoğu (ve birçok grafik projesiyle çalıştım) kuyruk çağrısı optimize edilemez . Basit faktöriyör gibi, durumu oluşturmak için özyineleme kullanır ve kuyruk işlemi birleşimdir.
Kuyruk çağrısı optimize edilebilir kod için, bu kodu tekrarlanabilir bir forma çevirmek genellikle kolaydır. Örneğin, sum()
daha önce gösterdiğim bu işlev genelleştirilebilir foldLeft()
. Kaynağa bakarsanız , bunun aslında yinelemeli bir işlem olarak uygulandığını göreceksiniz. Jörg W Mittag'ın işlev çağrıları yoluyla uygulanan bir durum makinesi örneği vardı; atlamalara çevrilen işlev çağrılarına dayanmayan çok sayıda verimli (ve bakım yapılabilir) durum makinesi uygulaması vardır.
Tamamen farklı bir şeyle bitireceğim. SICP'deki dipnotlardan Google'a doğru yol alırsanız, buraya gelebilirsiniz . Ben şahsen benim derleyici yerine sahip altında olması durumunda çok daha ilginç bir yer bulmak JSR
tarafından JUMP
.