JVM, kuyruk çağrısı optimizasyonuna hangi kısıtlamaları uygular?


36

Clojure, kuyruk çağrısı optimizasyonunu kendi başına yapmaz: kuyruk özyinelemeli bir işleve sahip olduğunuzda ve optimize edilmesini istediğinizde, özel formu kullanmanız gerekir recur. Benzer şekilde, karşılıklı olarak iki özyinelemeli işleviniz varsa, bunları yalnızca kullanarak optimize edebilirsiniz trampoline.

Scala derleyicisi özyinelemeli bir işlev için TCO'yu gerçekleştirebilir, ancak iki özyinelemeli işlev için yapamaz.

Ne zaman bu sınırlamaları okuduğumda, her zaman JVM modeline özgü bazı sınırlamalara bağlandılar. Derleyiciler hakkında hiçbir şey bilmiyorum, ama bu beni biraz şaşırtıyor. Örnek alalım Programming Scala. İşte işlev

def approximate(guess: Double): Double =
  if (isGoodEnough(guess)) guess
  else approximate(improve(guess))

çevrildi

0: aload_0
1: astore_3
2: aload_0
3: dload_1
4: invokevirtual #24; //Method isGoodEnough:(D)Z
7: ifeq
10: dload_1
11: dreturn
12: aload_0
13: dload_1
14: invokevirtual #27; //Method improve:(D)D
17: dstore_1
18: goto 2

Yani, bytecode düzeyinde, sadece bir ihtiyacı var goto. Bu durumda, aslında, zor iş derleyici tarafından yapılır.

Temel sanal makinenin hangi tesisi derleyicinin TCO'yu daha kolay ele almasına izin verir?

Bir yandan not olarak, gerçek makinelerin JVM'den daha akıllı olmasını beklemem. Yine de, Haskell gibi yerel kodları derleyen birçok dilde, kuyruk çağrılarını optimize etme konusunda bir sorun yok gibi görünüyor (peki, Haskell'in bazen tembellik nedeniyle olabilir, ama bu başka bir konudur).

Yanıtlar:


25

Şimdi, Clojure hakkında fazla bir şey bilmiyorum ve Scala hakkında çok az şey biliyorum ama bir şans vereceğim.

Öncelikle, kuyruk ÇAĞRILARI ile kuyruk Alımı arasında ayrım yapmamız gerekir. Kuyruk özyineleme gerçekten bir döngüye dönüştürmek kolaydır. Kuyruk çağrıları ile genel durumda imkansız olmak çok daha zor. Ne dendiğini bilmek zorundasınız, ancak polimorfizm ve / veya birinci sınıf fonksiyonlarla nadiren bunu biliyorsunuz, böylece derleyici aramanın nasıl değiştirileceğini bilemez. Yalnızca çalışma zamanında hedef kodu bilirsiniz ve başka bir yığın çerçevesini ayırmadan oraya atlayabilirsiniz. Örneğin, aşağıdaki parça kuyruk çağrısı yapar ve uygun şekilde optimize edildiklerinde (TCO dahil) herhangi bir yığın alanına ihtiyaç duymaz, ancak JVM için derlenirken elimine edilemez:

function forward(obj: Callable<int, int>, arg: int) =
    let arg1 <- arg + 1 in obj.call(arg1)

Burada sadece biraz verimsiz olmasına rağmen, tonlarca çağrı yapan ve nadiren geri dönen tüm programlama stilleri (Devamlı Geçiş Stili veya CPS gibi) vardır. Bunu, tam TCO olmadan yapmak, yığın alanını doldurmadan önce yalnızca küçük kod parçalarını çalıştırabileceğiniz anlamına gelir.

Temel sanal makinenin hangi tesisi derleyicinin TCO'yu daha kolay ele almasına izin verir?

Lua 5.1 VM'deki gibi bir kuyruk çağrısı talimatı. Örneğiniz çok kolaylaşmıyor. Benimki böyle bir şey olur:

push arg
push 1
add
load obj
tailcall Callable.call
// implicit return; stack frame was recycled

Bir kısaca, gerçek makinelerin JVM'den daha akıllı olmasını beklemem.

Haklısın, değiller. Aslında, daha az zekidirler ve böylece yığın çerçeveleri gibi şeyleri bile bilmiyorlar (çok). İşte tam da bu nedenle, istifleme alanını yeniden kullanmak ve bir dönüş adresi zorlamadan koda atlamak gibi püf noktaları çekilebilir.


Anlıyorum. Daha az akıllı olmanın , başka türlü yasaklanacak bir optimizasyona izin verebileceğinin farkında değildim .
Andrea

7
+1, tailcallJVM'ye yönelik talimatlar zaten 2007'nin başlarında önerildi: way.com'da sun.com'daki blog . Oracle devraldıktan sonra, bu bağlantı 404's. Sanırım JVM 7 öncelik listesine girmedi.
K.Steff,

1
Bir tailcalltalimat, yalnızca bir kuyruk çağrısını kuyruk çağrısı olarak işaretler. JVM'nin daha sonra gerçekten optimize edilmiş olup olmadığı, bahsedilen kuyruk çağrısının tamamen farklı bir sorudur. CLI CIL'in bir .tailtalimat öneki var, ancak uzun süredir Microsoft 64 bit CLR bunu optimize etmedi. OTOH, IBM J9 JVM , hangi aramaların kuyruk aramaları olduğunu söylemek için özel bir talimat gerekmeden, kuyruk aramalarını algılar ve optimize eder. Açıklamalı kuyruk aramaları ve kuyruk aramalarını optimize etmek gerçekten diktir. (Hangi aramanın bir kuyruk çağrısı olduğu statik olarak düşülmesinin gerçeğe uygun olmasının yanı sıra kararsız olabilir veya olmayabilir.)
Jörg W Mittag

@ JörgWMittag İyi bir noktaya değindiniz, bir JVM deseni kolayca tespit edebiliyor call something; oreturn. Bir JVM spec güncellemesinin birincil işi, açık bir kuyruk çağrısı talimatı sunmak değil , böyle bir talimatın optimize edilmesini zorunlu kılmak olacaktır . Böyle bir talimat, derleyici yazarlarının işlerini sadece kolaylaştırır: JVM yazarı, tanıma ötesine geçmeden önce talimat sırasını tanıdığından emin olmak zorunda değildir ve X-> bytecode derleyicisi bayt kodlarının ya geçersiz ya da geçersiz olduğundan emin olabilir aslında en iyi duruma getirilmiş, hiçbir zaman doğru değil yığın taşması.

@delnan: Dizi call something; return;ancak kuyruk çağrısı ile aynı olacaktır, eğer denilen şey hiçbir zaman yığın izi istemezse ; Söz konusu yöntemin sanal olması veya sanal bir yöntem çağrılması durumunda, JVM'nin yığın hakkında bilgi isteyip istemediğini bilmesi mümkün olmayacaktır.
supercat,

12

Clojure olabilir döngüler içine kuyruk özyinelemenin otomatik optimizasyon gerçekleştirin: o Scala kanıtlıyor olarak JVM bunu yapmak kesinlikle mümkündür.

Aslında bunu yapmamak bir tasarım kararıydı - recurbu özelliği istiyorsanız özel formu açıkça kullanmanız gerekir . Posta başlığına bakın Re: Neden Clojure google grubunda kuyruk çağrısı optimizasyonu yok ?

Mevcut JVM'de yapılması imkansız olan tek şey, farklı işlevler arasında kuyruk çağrısı optimizasyonu (karşılıklı özyineleme). Bu, uygulanması özellikle karmaşık değildir (Scheme gibi diğer diller baştan beri bu özelliğe sahipti) ancak JVM şartnamesinde değişiklik yapılmasını gerektirecekti. Örneğin, tüm işlev çağrısı yığınını korumakla ilgili kuralları değiştirmeniz gerekir.

JVM'nin gelecekteki bir yinelemesi, muhtemelen eski bir kod için geriye dönük uyumlu davranış sürdürülecek bir seçenek olarak olsa da, bu kabiliyeti elde etmek için muhtemeldir. Diyelim, Geeknizer'daki Özellikler Önizlemesi , Java 9 için şunu listeler:

Kuyruk çağrıları ve süreklilik ekleme ...

Tabii ki, gelecekteki yol haritaları her zaman değişebilir.

Görünüşe göre, zaten önemli değil. Clojure kodlama 2 yılı aşkın, ben var asla TCO eksikliği sorunu oldu durumlarda çalışabilir. Bunun ana nedenleri:

  • Hali hazırda% 99 oranında recurveya bir döngüde hızlı kuyruk özyineleme elde edebilirsiniz . Karşılıklı kuyruk özyineleme durumu normal kodda oldukça nadirdir
  • Karşılıklı özyinelemeye ihtiyaç duyduğunuzda bile, özyineleme derinliği TCO'suz her neyse istif üzerinde yapabileceğiniz kadar sığdır. TCO sonuçta sadece bir "optimizasyon".
  • Bir miktar yığın tüketmeyen karşılıklı özyinelemeye ihtiyaç duyduğunuz çok nadir durumlarda, aynı amacı gerçekleştirebilecek çok sayıda alternatif vardır: tembel sekanslar, trambolinler vs.

"future iteration" - Geeknizer'da Özellikler Önizlemesi Java 9 için diyor: Kuyruk aramaları ve süreklilik ekleme - öyle mi?
gnat

1
Evet - işte bu. Tabii ki, gelecekteki yol haritaları her zaman değişebilir ....
mikera

5

Bir kısaca, gerçek makinelerin JVM'den daha akıllı olmasını beklemem.

Daha akıllı olmak değil, farklı olmakla ilgili. Yakın zamana kadar, JVM sadece çok katı bir belleğe ve çağıran modellere sahip olan tek bir dil (Java, açıkçası) için özel olarak tasarlanıp optimize edildi.

Sadece herhangi bir işaret gotoya da işaretçi yoktu, 'çıplak' bir işlev çağırmanın bile bir yolu yoktu (bir sınıf içinde tanımlanmış bir yöntem değildi).

Kavramsal olarak, JVM'yi hedeflerken, bir derleyici yazar "bu kavramı Java terimlerinde nasıl ifade edebilirim?" Diye sormalıdır. Ve açıkça, TCO'yu Java ile ifade etmenin bir yolu yok.

Bunların JVM'nin başarısızlığı olarak görülmediğini, çünkü Java için gerekli olmadıklarını unutmayın. Java bunun gibi bir özelliğe ihtiyaç duyduğunda, JVM'ye eklenir.

Sadece son zamanlarda Java yetkilileri, Java dışındaki diller için bir platform olarak JVM'yi ciddiye almaya başladıklarından, Java eşdeğeri olmayan özellikler için bir miktar destek kazanmıştır. En bilinenleri, JVM'de bulunan ancak Java'da olmayan dinamik yazmadır.


3

Yani, bytecode düzeyinde, sadece bir kişinin gotoya ihtiyacı var. Bu durumda, aslında, zor iş derleyici tarafından yapılır.

Yöntem adresinin 0 ile başladığını fark ettiniz mi? Yani bütün yöntemler ofsets 0 ile başlamıyorsa? JVM, birinin bir yöntemin dışına atlamasına izin vermiyor.

Yöntemin dışında java tarafından yüklenen dışında bir dalda ne olacağı hakkında hiçbir fikrim yok - belki bytecode doğrulayıcısı tarafından yakalanır, belki bir istisna oluşturur ve belki de yöntemin dışına çıkar.

Sorun elbette, aynı sınıfın diğer yöntemlerinin nerede olacağını, diğer sınıfların yöntemlerinin çok daha az olacağını garanti edemezsiniz. JVM'nin yöntemleri nereye yükleyeceği konusunda herhangi bir garanti verdiğinden şüpheliyim, ancak düzeltilmekten memnuniyet duyarım.


İyi bir nokta. Ancak kuyruk çağrısı kendi kendine özyinelemeli bir işlevi optimize etmek için tek ihtiyacınız olan, aynı yöntem içinde bir GOTO . Dolayısıyla bu sınırlama özyinelemeli yöntemlerin TCO'sunu dışlamaz.
Alex D
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.