Neden iç içe geçmiş işlev çağrıları devre dışı bırakılabiliyorsa, programlar neden çağrı yığınlarını kullanıyor?


32

Derleyiciye neden böyle bir program almadıysanız:

function a(b) { return b^2 };
function c(b) { return a(b) + 5 };

ve böyle bir programa dönüştürün:

function c(b) { return b^2 + 5 };

böylece bilgisayarın c (b) 'nin dönüş adresini hatırlama ihtiyacını ortadan kaldırarak?

Sanırım artan sabit disk alanı ve programı depolamak ve derlemesini desteklemek için gereken RAM (sırasıyla) çağrı yığınlarını kullanmamızın nedeni. Bu doğru mu?


30
Bunu anlamlı boyutta bir programda yaparsanız ne olacağını görün. Özellikle, fonksiyonlar birden fazla yerden çağrılır.
user253751

10
Ayrıca, bazen derleyici hangi fonksiyonun çağrıldığını bilmez! Aptal örnek:window[prompt("Enter function name","")]()
user253751

26
function(a)b { if(b>0) return a(b-1); }Bir yığın olmadan nasıl uygularsınız ?
pjc50

8
İşlevsel programlama ile ilişki nerede?
mastov

14
@ pjc50: Kuyruğu özyinelemeli, bu yüzden derleyici değiştirilebilen bir döngüye çevirir b. Ancak, ele alındığında, özyinelemeli işlevlerin tümü özyinelemeyi ortadan kaldıramaz ve işlev prensipte olsa bile, derleyici bunu yapacak kadar akıllı olmayabilir.
Steve Jessop,

Yanıtlar:


75

Buna "iç içe geçme" denir ve çoğu derleyici bunu mantıklı olduğu durumlarda bir optimizasyon stratejisi olarak yapar.

Özel örneğinizde, bu optimizasyon hem alan hem de uygulama süresinden tasarruf sağlayacaktır. Fakat eğer fonksiyon programda birden fazla yerde çağrılırsa (nadir değil!), Kod boyutunu arttırır, böylece strateji daha şüpheli hale gelir. (Ve eğer bir fonksiyon doğrudan veya dolaylı olarak kendini çağırırsa, satır içi imkansız olacaktı, o zamandan beri kod sonsuz olacaktı.)

Ve tabii ki, sadece "özel" işlevler için mümkündür. Harici arayanlar için açığa çıkan fonksiyonlar, en azından dinamik bağlantıya sahip dillerde değil, optimize edilemez.


7
@Blrfl: Modern derleyiciler aslında başlıkta tanımlara ihtiyaç duymazlar; Çeviri Birimleri arasında satır içi olabilirler. Bu olsa iyi bir bağlayıcı gerektirir. Başlık dosyalarındaki tanımlar aptal linkerler için geçici bir çözümdür.
MSalters

3
"Harici arayanlar için açığa çıkan fonksiyonlar optimize edilemez" - fonksiyonun var olması gerekir, ancak kendisine verilen herhangi bir çağrı sitesi (kendi kodunuzda veya kaynak varsa, harici arayanlar ') sıralanabilir.
Random832

14
Vay, 28, herşeyin içini çizmenin imkansız olmasının nedenini bile belirtmeyen bir cevap için can atıyor: Özyineleme.
mastov

3
@R ..: LTO LINK zaman optimizasyonu, LOAD Zaman Optimizasyonu değil.
MSalters

2
@immibis: Fakat bu açık yığın derleyici tarafından tanıtıldı, sonra yığını olduğunu olduğunu çağrı yığını.
user2357112

51

Sorunuz için iki bölüm var: Neden birden fazla fonksiyona sahip olmaktasınız (fonksiyon çağrıları tanımlarıyla değiştirmek yerine) ve bu fonksiyonları neden verilerini statik olarak başka bir yere tahsis etmek yerine çağrı yığınlarıyla uygulayın?

İlk sebep özyinelemedir. Sadece "ah, bu listedeki her bir öğe için yeni bir işlev çağrısı yapalım" türünden değil, aynı zamanda etkin bir işlev için iki çağrınızın olabileceği mütevazı türden biri. Bunu desteklemek için yerel değişkenleri bir yığına koymanız gerekir ve genel olarak özyinelemeli işlevleri satır içi yapamazsınız.

Öyleyse kütüphaneler için bir sorun var: Nerede ve ne sıklıkta hangi işlevlerin çağrılacağını bilmiyorsunuz, bu nedenle bir "kütüphane" hiçbir zaman derlenemedi, yalnızca tüm müşterilere uygun bir üst düzey biçiminde gönderildi. uygulamaya dahil edildi. Bununla ilgili diğer sorunların yanı sıra, tüm avantajlarıyla dinamik bağlantıyı tamamen kaybedersiniz.

Ek olarak, yapabilseniz bile işlevleri satır içi değil yapmanın birçok nedeni vardır:

  1. Daha hızlı olması gerekmez. Yığın çerçevesinin ayarlanması ve yırtılması, yürütme sürelerinin% 0.1'i kadar olmayan birçok büyük veya döngü fonksiyonu için belki de bir düzine tek çevrim talimattır.
  2. Daha yavaş olabilir. Kodun çoğaltılmasının maliyeti vardır, örneğin, talimat önbelleğine daha fazla baskı uygular.
  3. Bazı işlevler çok büyüktür ve birçok yerden çağrılır, onları her yere dizmek, makul olanın çok ötesinde ikilik artar.
  4. Derleyiciler genellikle çok büyük işlevlerle zor zamanlar geçirir. Geriye kalan her şey eşit, 2 * N büyüklüğünde bir işlev 2 N büyüklüğünde bir işlev alır, N büyüklüğünde bir işlev T zaman alır.

1
4. noktaya şaşırdım. Bunun sebebi nedir?
JacquesB

12
@JacquesB Birçok optimizasyon algoritması, karesel, kübik veya hatta teknik olarak NP-tamamlandı. Kanonik örnek, grafik renklendirmeyle analojiyle NP tamamlanmış kayıt tahsisidir. (Genellikle derleyiciler kesin bir çözümü denemez, ancak yalnızca birkaç çok sezgisel sezgisel tarama yaparlar.) Pek çok basit, tek geçişli optimizasyonların önce kontrol akışlarında baskınlığa bağlı olan her şey gibi (genellikle genel olarak süper doğrusal analiz geçişi gerekir) n log n n temel bloklarla).

2
“Burada gerçekten iki sorum var” Hayır, bilmiyorum. Birincisi - neden bir fonksiyon çağrısını derleyicinin çağrılan fonksiyonun koduyla değiştirebileceği bir yer tutucu olarak ele almıyorsunuz?
moonman239

4
@ moonman239 Sonra ifadeleriniz beni attı. Yine de, soru olabilir benim cevap olduğu gibi ayrıştırılabilir ve bunun yararlı bir perspektif olduğunu düşünüyorum.

16

Yığınlar, sonlu kayıt sayıları tarafından belirlenen sınırları zarif bir şekilde atlamamıza izin verir.

Tam olarak 26 küre "az" kaydettirdiğini düşünün (veya 8080 yongasının sadece 7 byte büyüklüğünde kayıtları bile vardır) Ve bu uygulamada yazdığınız her işlev bu düz listeyi paylaşır.

Naif bir başlangıç ​​ilk birkaç kayıt ilk işleve tahsis etmek ve sadece 3 olduğunu bilmek, ikinci işlev için "d" ile başlamaktır ... Hızlı bir şekilde tükeniyor.

Bunun yerine, turbo makinesi gibi metaforik bir kasanız varsa, her bir işlevin kaseti kullanıp ileri ve geri () yerleştirerek "başka bir işlev çağrısı" başlatmasını sağlayabilirsiniz; istediği gibi kaydeder. Callee bittiğinde, callee'nin çıktısını gerektiği gibi nereye takacağını bilen ve sonra kaseti geriye döndürmek için kaseti geriye doğru çalar.

Temel çağrı çerçeveniz tam olarak budur ve standart makine kodu dizileri tarafından derlenir ve düşürülür, derleyici bir işlevden diğerine geçişlerin etrafına koyar. (C yığın çerçevelerimi hatırlamak zorunda olduğumdan bu yana çok zaman geçti, ancak X86_calling_conventions'da görevini bırakanların görevlerini okuyabiliyorsunuz .)

(özyineleme harika, ancak hiç bir kayıt olmadan yığınları dengelemek zorunda kalsaydınız, yığınları gerçekten takdir edersiniz .)


Sanırım artan sabit disk alanı ve programı depolamak ve derlemesini desteklemek için gereken RAM (sırasıyla) çağrı yığınlarını kullanmamızın nedeni. Bu doğru mu?

Bugünlerde daha fazla satır içi yapabilsek de ("daha fazla hız" her zaman iyidir; "daha az kb montaj" demek, video akış dünyasında çok az anlamına gelir).

Örneğin, polimorfik nesneler - verilecek tek ve tek nesne türünü bilmiyorsanız, düzleştiremezsiniz; Nesnenin özellikleri değişkenine bakmak ve bu işaretçiyi çağırmak zorundasınız ... çalışma zamanında yapılması önemsiz, derleme zamanında satır içi imkansız

Modern bir alet zinciri, arayan kişinin (lerin) tam olarak hangi objenin lezzetinin ne olduğunu bilmesi için yeterince düzleştiğinde polimorfik olarak tanımlanmış bir fonksiyonun içine girebilir :

class Base {
    public: void act() = 0;
};
class Child1: public Base {
    public: void act() {};
};
void ActOn(Base* something) {
    something->act();
}
void InlineMe() {
    Child1 thingamabob;
    ActOn(&thingamabob);
}

Yukarıdakilerde, derleyici InlineMe'den içsel hareketine () ne olursa olsun () sırasında çalışma anında herhangi bir viyola dokunma gereksinimine kadar statik olarak inline etmeye devam etmeyi seçebilir.

Ama nesnenin hangi lezzet herhangi belirsizlik aynı işlevin diğer bazı dualar bile, ayrı bir işleve çağrısı olarak bırakacağım edilir inlined.


11

Bu yaklaşımın kaldıramadığı durumlar:

function fib(a) { if(a>2) return fib(a-1)+fib(a-2); else return 1; }

function many(a) { for(i = 1 to a) { b(i); };}

Orada olan sınırlı ya da hiç arama yığınlarını ile dil ve platformlar. PIC mikroişlemcilerinde 2 ila 32 girişle sınırlı bir donanım yığını bulunur . Bu tasarım kısıtlamaları yaratır.

COBOL recursion'u yasakladı: https://stackoverflow.com/questions/27806812/in-cobol-is-it-possible-to-recursively-call-a-paragraph

Özyineleme yasağı koymak, programın tüm çağrı belgesini statik olarak DAG olarak temsil edebileceğiniz anlamına gelir. Derleyiciniz, geri dönüş yerine sabit bir atlama ile çağrıldığı her yer için bir işlevin bir kopyasını yayabilir. Yığın yok, sadece daha fazla program alanı, karmaşık sistemler için oldukça fazla potansiyel. Ancak, küçük gömülü sistemler için bu, çalışma zamanında yığın taşması olmamasını garanti edebileceğiniz anlamına gelir; bu, nükleer reaktörünüz / jet türbininiz / araç gaz kelebeği kontrolünüz vb. İçin kötü haber olabilir.


12
İlk örneğiniz temel özyineleme ve orada haklısınız. Fakat ikinci örneğiniz başka bir işlevi çağıran bir for döngüsü gibi görünüyor. İşlevin iç içe geçmesi, bir döngünün açılmasından farklıdır; fonksiyon, döngüyü açmadan dizilmiş olabilir. Yoksa bazı ince ayrıntıları kaçırdım mı?
jpmc26

1
İlk örneğiniz Fibonacci serisini tanımlamaksa, yanlıştır. (Bir fibarama eksik .)
Paŭlo Ebermann

1
Yasaklı özyinelemeyi yasaklamak, tüm çağrı grafiğinin bir DAG olarak temsil edilebileceği anlamına gelse de, bu, iç içe geçmiş çağrı dizilerinin tam listesini makul bir alanda listeleyebileceği anlamına gelmez. 128KB kod alanına sahip bir mikrodenetleyici için bir projemde, maksimum parametre-RAM gereksinimini etkileyebilecek tüm işlevleri içeren ve bu çağrı grafiğinin bir konser üzerinde olduğunu gösteren bir çağrı grafiği isteme hatası yaptım. Tam bir arama grafiği daha uzun olurdu ve bu 128K kod alanına uyan bir program içindi.
supercat

8

İşlev satır içi istersiniz ve çoğu (en iyi duruma getirme ) derleyiciler bunu yapar.

Inlineing'in çağrılan fonksiyonun bilinmesini gerektirdiğine dikkat edin (ve sadece çağrılan fonksiyon çok büyük değilse etkilidir), çünkü çağrının yerine çağrılan functgion'un yerine yazmanın yerini alır. Yani genel olarak olamaz satır içi bilinmeyen bir fonksiyon (örneğin bir işlev işaretçisi -ve işlevleri içeren dinamik bağlantılı paylaşılan kütüphaneler - bazı sanal yöntem olarak belki görülebilir vtable ; ancak aracılığıyla bazı derleyiciler olabilir bazen optimize devirtualization teknikleri). Elbette özyinelemeli işlevler satır içi her zaman mümkün değildir (bazı akıllı derleyiciler kısmi değerlendirme kullanabilir ve bazı durumlarda özyinelemeli işlevler satır içi yapabilir).

Ayrıca, kolay bir şekilde mümkün olsa bile, satır çizmenin her zaman etkili olmadığına dikkat edin: siz (aslında derleyiciniz) CPU önbelleklerinin (veya dal belirleyicisinin ) daha az verimli çalışacağı ve programınızın çalışmasını sağlayacağı kod boyutu o kadar artabilir Yavaş.

Sorgularınızı bu şekilde etiketlediğiniz için biraz işlevsel programlama stiline odaklanıyorum .

Herhangi bir çağrı yığını olması gerekmediğine dikkat edin (en azından "çağrı yığını" ifadesinin makine anlamında). Sadece yığını kullanabilirsiniz.

Yani, bir almak bakmak devamlılık ve hakkında daha fazla bilgi devamı geçen tarzı (CPS) ve CPS dönüşümü (sezgisel, sen devam kullanabilirsiniz kapanışları yığın ayrılmış şeyleşmiş "çağrı çerçeveleri" olarak, ve bir çağrı yığını taklit sort-vardır; o zaman verimli bir çöp toplayıcıya ihtiyacınız vardır ).

Andrew Appel , Continuations ile Derleme adlı bir kitap yazdı ve eski bir kağıt çöp koleksiyonu yığın dağıtımından daha hızlı olabilir . Ayrıca bakınız A.Kennedy'nin makalesi (ICFP2007) Devam Ediyor, Devam Ediyor

Devamlılık ve derleme ile ilgili birkaç bölümden oluşan Queinnec'in Lisp In Küçük Parçalar kitabını da okumanızı tavsiye ederim .

Ayrıca bazı dillerin (örn. Brainfuck ) veya soyut makinelerin (örn. OISC , RAM ) herhangi bir arama özelliğine sahip olmadığına, ancak hala Turing-tamamlanmış olduğuna dikkat edin; bu son derece uygun. BTW, bazı eski komut seti mimarileri (örneğin IBM / 370 ) bir donanım çağrısı yığına veya bir itme çağrısı makinesi talimatına (IBM / 370 yalnızca bir Şube ve Bağlantı makinesi talimatına sahipti ) bile yok

Sonunda, tüm programınız (gerekli tüm kütüphaneleri de içeren) herhangi bir özyinelemeye sahip değilse, her bir fonksiyonun dönüş adresini (ve aslında statik hale gelen "yerel" değişkenleri) statik konumlarda saklayabilirsiniz. Eski Fortran77 derleyicileri bunu 1980'lerin başında yaptı (yani derlenen programlar o zamanlar herhangi bir çağrı yığını kullanmadı).


2
Çok tartışmalı, CPS'nin "çağrı yığını" yok. Yığın üzerinde değil, sıradan RAM'in mistik bölgesi olan ve biraz donanım desteğine sahip olan %espvb., Ancak yine de başka bir RAM bölgesinde uygun bir şekilde adlandırılmış spagetti yığını üzerindeki eşdeğer defter tutma tutar. Özellikle iade adresi, devamında kodlanmıştır. Ve tabii ki devam etmeleri daha hızlı değil (ve bana öyle geliyor ki, OP de ne elde ediyordu) inlining ile hiç arama yapmamaktan başka .

Appel'in eski raporları, (ve kıyaslama ile gösterildi) CPS'nin bir çağrı yığını olması kadar hızlı olabileceğini iddia etti.
Basile Starynkevitch 12:15

Bundan şüpheliyim ama iddia ettiğim şey bu değil.

1
Aslında, bu 1980'lerin sonunda MIPS iş istasyonundaydı. Muhtemelen, mevcut bilgisayarlardaki önbellek hiyerarşisi performansı biraz farklı kılacaktır. Appel'in iddialarını analiz eden birkaç makale var (ve aslında mevcut makinelerde yığın tahsisi, dikkatlice işlenmiş çöp toplamadan biraz daha hızlı olabilir )
Basile Starynkevitch

1
@Gilles: Cortex M0 ve M3 (ve muhtemelen M4 gibi diğerleri) gibi birçok yeni ARM çekirdeği, kesme işlemesi gibi şeyler için donanım yığını desteğine sahiptir. Ayrıca, Thumb komut seti, RR ile / LR'nin herhangi bir kombinasyonu ile STRMDB R13 ve LR ile / olmadan herhangi bir R0-R7 kombinasyonunun ve PC ile / olmadan, herhangi bir R0-R7 kombinasyonunun LDRMIA R13'ü içeren sınırlı bir alt kümesini içerir. Bir yığın işaretçisi olarak R13.
supercat

8

Satır içi (işlev çağrılarının eşdeğer işlevlerle değiştirilmesi) küçük basit işlevler için bir optimizasyon stratejisi olarak iyi çalışır. Bir işlev çağrısının ek yükü, ek program boyutunda (veya bazı durumlarda hiçbir ceza olmadan) küçük bir ceza için etkin bir şekilde takas edilebilir.

Bununla birlikte, diğer işlevleri de içeren büyük işlevler, her şey belirtildiyse program boyutunda büyük bir patlamaya neden olabilir.

Çağrılabilir fonksiyonların tüm amacı, sadece programcı tarafından değil, makinenin kendisi tarafından ve verimli bellek kullanımını veya makul bellek veya disk üstü ayakizi gibi özellikleri içeren yeniden kullanımın kolaylaştırılmasıdır.

Buna değer: bir çağrı yığını olmadan çağrılabilir işlevlere sahip olabilirsiniz. Örneğin: IBM System / 360. Bu donanımdaki FORTRAN gibi dillerde programlama yaparken, program sayacı (dönüş adresi), fonksiyon giriş noktasının hemen önünde bulunan küçük bir hafıza bölümüne kaydedilecektir. Yeniden kullanılabilir işlevlere izin verir, ancak özyinelemeli veya çok iş parçacıklı kodun yapılmasına izin vermez (özyinelemeli veya yeniden girilen bir arama girişimi daha önce kaydedilmiş bir iade adresinin üzerine yazılmasına neden olur).

Diğer cevaplarla açıklandığı gibi, yığınlar iyi şeylerdir. Özyinelemeyi ve çok iş parçacıklı çağrıları kolaylaştırır. Yinelemeyi kullanmak için kodlanan herhangi bir algoritma yinelemeye güvenmeksizin kodlanabilse de, sonuç daha karmaşık, bakımı zor ve daha az verimli olabilir. Yığın içermeyen bir mimarinin çoklu iş parçacığını destekleyebileceğinden emin değilim.

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.