Antik Dünyada Alt Program Çağrısı: Bilgisayarlar Stack veya Heap’e Sahip Olmadan Önce
(devblogs.microsoft.com)- İlk bilgisayarlar, stack ve heap olmadan da fonksiyon çağrılarını gerçekleştirmek zorundaydı; derleyiciler çağrı durumunu parametreler, dönüş adresi ve yerel değişkenlere karşılık gelen gizli global değişkenlerle yönetiyordu
- Çağıran taraf argümanları kaydedip dönüş adresi değişkenine geri dönülecek konumu yazdıktan sonra fonksiyonun başlangıç noktasına atlar, fonksiyon da hesaplamayı bitirdikten sonra kaydedilmiş adrese yeniden atlar
- Mantıksal yerel değişkenler de aslında global depolama alanını kullandığı için, dışarıdan fonksiyon gibi görünse de iç işleyiş sabit bellek ve
gotoya daha yakındı - Bazı ABI’ler ve işlemciler argüman aktarımı ile dönüş adresi işlemeyi register’lar veya
branch with linkile optimize etse de temel kısıt değişmeden kaldı - Aynı fonksiyonun dönüş adresi yeni bir çağrıyla üzerine yazıldığı için özyinelemeli çağrı mümkün değildi; dönemin dilleri buna özyinelemeyi yasaklayarak ya da yalnızca açıkça izin verildiğinde destekleyerek karşılık verdi
Stack olmadan fonksiyon çağrısı nasıl kurulur
- Erken dönem bilgisayar ortamlarında bugün doğal kabul edilen stack veya heap yoktu
- Heap olmadan dinamik bellek ayırma, sabit boyutlu tamponlarla ikame edilebiliyordu
- Değişken boyutlu veriler işlenirken bile önceden yeterince büyük sabit tamponlar ayrılırdı
- İstenen veri tampon kapasitesini aşarsa program ölümcül bir hatayla sonlandırılırdı
- Daha kullanıcı dostu uygulamalar, en yüksek kapasitenin derleme sırasında ayarlanmasına izin verirdi
- Daha gelişmiş uygulamalar, sabit tamponun üstüne özel bir ayırıcı koyup bunu
allocatevefreebenzeri şekilde kullanabilirdi
Gizli global değişken tabanlı çağrı kuralı
- Derleyiciler stack olmadan fonksiyon çağrısı yapabilmek için her fonksiyon için birkaç gizli global değişken tanımlardı
- Her giriş parametresi için bir global değişken
- Fonksiyonun dönüş adresini tutan bir global değişken
- Yerel değişkenlere karşılık gelen global değişkenler
- Çağrı kodu şu sırayla çalışırdı
- Parametre değerleri ilgili gizli global değişkenlere yazılırdı
- Geri dönülecek konum, fonksiyonun dönüş adresi değişkenine kaydedilirdi
- Fonksiyonun başlangıç konumuna
gotoile atlanırdı
- Fonksiyon, hem parametreleri hem de yerel değişkenleri bu gizli global değişkenlerden okur ve onlara yazar
- İş bitince dönüş değeri dönüş değeri register’ına konur ve fonksiyonun dönüş adresi değişkeninde saklanan adrese atlanırdı
C benzeri kodun goto tabanlı koda dönüşmesine örnek
- Örnek fonksiyon
add_two_values(int a, int b), stack olmadan şu depolama alanlarına dönüştürülebilirdia2v_avea2v_b, argümanları saklayan global değişkenlerdia2v_c, yerelcdeğişkenine karşılık gelen global değişkendia2v_retaddr, geri dönüş adresini saklayan global değişkendi
- Çağıran
sample(),31415ve2718değerlerini sırasıyla argüman global değişkenlerine yazardı - Ardından
a2v_retaddriçineresumekonumunu koyupadd_two_valuesfonksiyonuna atlardı add_two_values, hesaplama sonucunureturn_value_registeriçine yazdıktan sonraa2v_retaddrüzerinden geri dönerdiresumekonumuna geri dönen çağıran taraf, dönüş değeri register’ındaki değerisample_xiçine kaydederdi
Register’lar ve branch with link ile optimizasyon
- Aynı yapı, ABI düzeyinde register üzerinden aktarım ile daha hızlı hale getirilebilirdi
- Birçok işlemci özel bir
link registervebranch with linkkomutu sunuyordubranch with link, dallanma komutundan sonraki komutun adresini otomatik olarak link register’a kaydeder- Çağıran taraf ilk iki argümanı
argument_register_1veargument_register_2içine koyabilirdi - Çağrılan fonksiyon bu register değerlerini kendi gizli global değişkenlerine taşıyıp kullanabilirdi
- Dönüş adresi de
link_registeriçinden fonksiyonun dönüş adresi değişkenine kaydedilebilirdi - Bu optimizasyon, stack olmadan da çağrı ve dönüş yapılabilmesi yönündeki temel yapıyı korurdu
Özyineleme neden engelleniyordu
- Bu çağrı yönteminin temel kısıtı, özyinelemeli çağrının mümkün olmamasıydı
- Özyinelemeli çağrı gerçekleştiğinde aynı fonksiyonun dönüş adresi değişkeni yeni çağrının dönüş adresiyle üzerine yazılırdı
- Dıştaki çağrı tamamlandığında aslında dönülmesi gereken özgün konum kaybolduğu için yanlış yere atlanırdı
- Dönemin programlama dilleri bu sorunu, özyinelemeyi desteklemeyerek aşardı
- FORTRAN başlangıçta alt programları bile desteklemiyordu; alt programlar 1958’de eklendi
- FORTRAN’da özyineleme desteğinin standart hale gelmesi 1991’i buldu ve o zaman bile alt programın
RECURSIVEolarak açıkça belirtilmesi gerekiyordu
Kendini değiştiren kod ve erken işlemcilerde alt program komutları
- Bazı derleyiciler daha da zekice davranıp kendini değiştiren kod kullanıyordu
- Fonksiyon sonundaki atlama komutunun içindeki adres alanı fiilen dönüş adresi değişkeni görevini görüyordu
- Bu yaklaşım yalnızca basit bir numara değil, pratik bir gereklilik de olabiliyordu
- Bazı işlemciler dolaylı atlamayı desteklemiyor olabilirdi
- Alt programların pratik değeri anlaşıldıktan sonra birçok işlemci özel çağrı komutları ekledi
- Dönüş adresi alt programın ilk sözcüğüne yazılırdı
- Asıl yürütme ikinci sözcükten başlardı
- Geri dönüş sırasında alt programın başlangıç etiketi üzerinden dolaylı atlama yapılırdı
- Örnek assembly’de
bsr add_two_values, dönüş adresiniadd_two_values’ın ilk sözcüğüne yazar ve kurbanlıknopsonrasındaki gerçek komutlardan yürütmeye başlar
1 yorum
Hacker News yorumları
Bu konuda The Art of Computer Programming gerçekten çok iyiydi
Dışarıdan bakınca eski görünebilir ama heap ya da stack öncesi dönemde dinamik olarak değişen dizilerle ve veri yapılarıyla uğraşan çok sayıda algoritma var
Kitap, çöp toplamaya ve Lisp listesi uygulamasına kadar adım adım ilerliyor; Knuth’tan bekleyeceğiniz ansiklopedik bilgi aynen mevcut
Özellikle sevdiğim örnek, iki dizinin tek bir alanı dinamik olarak paylaşma biçimi. Bir dizi
location#0konumundan ileri doğru, ikinci dizi iselocation#Endkonumundan geriye doğru büyürse, statik olarak ayrılmış alanı verimli biçimde paylaşmış oluyorsunuzBunu keyfî sayıda diziye genişletmek de mümkün, ama o noktada sadece
MallocveReallockullanmak daha iyi; tekniğin kendisi de malloc benzeri bir rutine oldukça yakınEkleme ve yapıştırma işlemlerinde veriyi kaydırmak gerekmiyordu, ama gezinirken gerekiyordu. Yine de iyi çalışıyordu
O kadar yer yoksa tercih edilenden daha küçük ayrılıyor, minimumu bile alamazsa çalıştırma başarısız oluyordu
Hatırladığım kadarıyla sistem, o fiziksel RAM parçasının alt tarafına heap’i ve kütüphaneleri, üst tarafına da stack’i yerleştiriyordu
System 8 civarında bir sanallaştırma katmanı eklenince bu yaklaşıma daha az ihtiyaç kaldı; MacOS X döneminde ise diğer sistemler gibi sayfalı bellek kullanıldığı için bu tür numaralara artık gerek kalmadı
Yine de Art of Computer Programming’deki bu tür “tuhaf tek numara”nın, aynı anda çalışan birden çok uygulama için RAM ayırma yöntemi olduğu dönemi düşünmek eğlenceli
Biri yukarı, diğeri aşağı doğru büyüyordu. Büyüleyici bir yapıydı ama vaat ettiği performansı sonunda veremedi
Sabit boyutlu bir sayfada offset dizisi ileri doğru büyür, değişken uzunluklu satır değerleri dizisi ise sondan geriye doğru büyür. Satır silindiğinde arka taraftaki dizide boşluklar oluşabildiğini anlıyorum
Dokümantasyon B-tree yapısının kendisi için TAOCP’ye atıf yaptığından, doğrudan ilham kaynağı olmuş olsa şaşırmam
ALGOL’e özyinelemeli fonksiyonlar eklemek epey tartışmalıydı ve hâlâ anlatması keyifli bir hikâye: https://vanemden.wordpress.com/2014/06/18/how-recursion-got-...
How recursion got into programming: intrigue, betrayal, and advanced semantics - https://news.ycombinator.com/item?id=33123916 - Ekim 2022, 8 yorum
How Recursion Got into Programming (2014) - https://news.ycombinator.com/item?id=23061881 - Mayıs 2020, 47 yorum
How recursion got into Algol 60: a comedy of errors - https://news.ycombinator.com/item?id=10131664 - Ağustos 2015, 124 yorum
How recursion got into programming: a comedy of errors - https://news.ycombinator.com/item?id=8073361 - Temmuz 2014, 108 yorum
SUBLEQ makinesi için bir Forth yorumlayıcısı (https://github.com/howerj/subleq) ve bit-serial makine için bir yorumlayıcı (https://github.com/howerj/bit-serial) yazmıştım; ikisinde de Forth için gereken fonksiyon çağrı yığını yoktu
SUBLEQ dolaylı yükleme/saklamaya da izin vermediğinden, azıcık karmaşık bir şey yapmak için kendi kendini değiştiren kod gerekiyor
İki makinede de bu işlevi yerine getirebilecek bir sanal makine oluşturup içine işbirlikçi multithreading de ekleyerek yaklaştım
Heap gerekiyorsa Forth ile yazıyorsunuz; kayan nokta word kümesini de Forth ile yazıyorsunuz. Birçok MCU’da hâlâ kayan nokta komutu yok ve bu, onu uygulayan yazılım fonksiyon çağrılarıyla halledilebiliyor
Bahsedilmese de diğer derleyicilerin de benzer bir yol izlediğini düşünüyorum. Bazı BASIC yorumlayıcıları da bir VM uygulayıp onu hedef aldı; P-Code da buna benzer
Temel sistem belleğinin çoğu video RAM’iydi ve video çipi yazmaçlarına
poke/peekyapmayı gerektiren epey zahmetli bir prosedürle erişilmesi gerekiyorduVideo çipi, otomatik artan bir geçerli bellek işaretçisini tutuyordu; ardışık okuma veya yazmada işaretçi 1 artıyordu. Ancak sistem belleğinin çoğuna yalnızca bu yolla erişilebilmesi, büyük programlar yazmayı başlı başına zorlaştırıyordu
Bu yüzden TI, bu video RAM erişimini daha doğal hâle getiren GPL adlı soyut bir makine oluşturdu. Ancak TMS9900 üzerinde yorumlanarak çalıştığı için native koddan yavaştı; ayrıca CPU video çipi RAM’ine yalnızca çipin ekran taraması yapmadığı yatay/dikey geri dönüş aralıkları gibi anlarda erişebildiğinden daha da yavaştı
BASIC kodu ve değişkenleri de tamamen bu video bellekteydi; dolayısıyla TI-99/4A’nın BASIC yorumlayıcısının neyle yazıldığı da ortada. Hiç hızlı değildi
İlginç olan nokta, TMS9900’de gerçek genel amaçlı yazmaçların olmamasıydı. WR0~WR15 çalışma alanı yazmaçları belleğin bir yerindeydi ve WP çalışma alanı işaretçisi yazmacı bunları gösteriyordu
CPU’nun fiziksel yazmaçları yalnızca PC, WP ve durum yazmacı olmak üzere üç taneydi. Sonuçta çok ilkel bir register windowing yapılabiliyordu;
BLWPkomutuyla dallanınca bellekte başka bir konumdaki yeni “yazmaç” kümesi etkinleşiyor ve dönüş adresi yeni çalışma alanına kaydediliyorduSon zamanlarda TI-99/4A’dan sık söz etmemin nedeni, kişisel proje olarak bu model için bir assembler geliştiriyor olmam
Bazı işlemcilerin dönüş adresini alt yordamın ilk komutundan hemen önceki word’e kaydettiği sözü doğru; PDP-8 böyle yapıyordu
PDP-8’in evrimi, özyineleme için donanım desteğine giden bir yolculuk olarak da görülebilir
Başta
JMSkomutu dönüş adresini fonksiyonun ilk word’üne gömüyordu. Çağıranın argümanlarıJMSkomutunun arkasına koyduğu, çağrılanın da dönüş komutuna göre ofsetlerle argümanları okuyup her seferinde artırarak dönüş adresinin tekrar kod konumunu göstermesini sağladığı durumlar da yaygındıDaha sonra otomatik artan konumlardan birini kullanarak basit bir stack yapmak oldukça yaygınlaştı. PDP-8’de işaretçi olarak her kullanıldığında artan 8 bellek konumu vardı; fonksiyon prologue/epilogue kodu bu stack’i doğrudan yöneterek tam özyinelemeyi mümkün kılıyordu
Daha sonra Harris 6120 gibi mikroişlemci uygulamalarına donanım stack’i eklenerek performans iyileştirildi
Rkomutu, yani dönüş adresi saklama komutu vardıBu komut, zaten artırılmış
PC+1değerini hedef konumdaki komutun adres kısmına kaydediyordu; geleneksel olarak bu hedef, alt yordamın başlangıcından hemen önceki koşulsuz dallanma komutuyduRkomutunun ardından ilgili alt yordama gidenUkoşulsuz dallanma komutu konurduAlt yordam, kendi önündeki adrese dallanarak dönerdi; orada da çağrı noktasının hemen sonrasına dönen koşulsuz dallanma bulunurdu
Daha gelişmiş bir çağrı konvansiyonu kullanılmadıkça özyineleme mümkün değildi. Ayrıca assembly dilindeki tüm komut kodları tek harfti
AVR-8 için yazılan programlarda C çağrı konvansiyonunu kullanmak bazen delilik gibi geliyor
Assembly kullanınca iç döngü değişkenlerini büyük yazmaç dosyasında sürekli tutabiliyor ya da yazıda anlatılan yöntemleri kullanabiliyorsunuz
Böyle uygulamalarda fonksiyonları “renklendirme” yöntemi de iyi. Kırmızı fonksiyonla yeşil fonksiyonun aynı anda etkin olmayacağını biliyorsanız, ikisinin yerel değişkenlerini veya parametrelerini yeniden kullanabilirsiniz
Geçmişte katıldığım bir mikrodenetleyici kod tabanı projesinde, birkaç geliştirici haftalar boyunca birden fazla alt sistemdeki yakalaması zor hataların izini sürüyordu
Kodu taşıdığınızda hata da onunla birlikte yer değiştiriyordu. Biraz iz sürüp tuzaklar kurunca, çağrı stack’inin fazla derinleşerek başka veri yapılarını ezdiği kod konumlarını bulabildik
Programlamayı ilk öğrenirken tam olarak bu şekilde programlamaya zorlanmıştım. 1970’lerde değil, 2001’de böyleydi
Çünkü ilk programlama deneyimim, oyun geliştirme aracı RPG Maker 2000’in sunduğu yarı grafiksel betik “dili”ydi
RM2K betiklemeyi hiç görmediyseniz Scratch ile Emacs Paredit modunun karışımını düşünün. Örnek: https://forums.rpgmakerweb.com/data/attachments/21/21958-f89...
Metin gibi görünür ama metin gibi düzenleyemezsiniz; yalnızca özellik iletişim kutuları olan bloklar olarak düzenlenir
Doğal olarak RPG Maker’ın betik dilinde stack gibi havalı şeyler yoktu. Yeniden kullanılabilir bir alt yordam gerekiyorsa parametreler için gizli global değişkenler ayırmanız gerekirdi; reentrant değildi
Geriye dönüp bakınca, yeterince inat edilseydi RPG Maker 2000 içinde hem register’lar hem de runtime stack uygulanabilirmiş gibi geliyor
Başta kolay görünüyor. 6502’nin zero page’i gibi sahte “register”lar oluşturabilir, dolaylı değişken erişimiyle (https://rpgmaker.net/tutorials/523/) bir stack de yapabilirsiniz
Sorun, RM2K’de “parallel process” betikleri biçiminde eşzamanlılığın olması. Bu tür soyutlamaları paralel süreçler kullanınca farklı “thread”ler durumu kafalarına göre birbirinin üzerine yazar
Bu yüzden her “sanal çekirdek” için birden çok zero page ve stack gerekir; her paralel betiğe de bir sanal çekirdek atamak/bağlamak/zamanlamak gerekir. Yani her betiğin, bir şekilde yalnızca kendisinin bildiği bir stack pointer’a sahip olması gerekir
Yarış durumlarında da kararlı kılmak için genellikle mutex gibi bir şeye ihtiyaç olur
RPG Maker oyun geliştiricilerinin inatçılığını düşününce, birilerinin bir runtime özelliğini kandırıp mutex gibi davranmasını sağlamanın yolunu bulmuş olacağını düşünüyorum; ama gerçekten ne yaptıklarını açıkçası bilmek istemeyecek kadar ürkütücü
rpgmaker.net’ten custom battle system uygulanmış bir oyun indirdiğimi hatırlıyorum. Yerleşik savaş sisteminin tamamını, anlattığınız türden tekniklerle söküp değiştiren bir uygulamaydı
Editörde açıp nasıl çalıştığına baktığımda tamamen afallamıştım. Yüzlerce “değişken” vardı; yanlış hatırlamıyorsam yalnızca i64’e izin veriyordu, ayrıca yüzlerce “switch” de vardı. Switch’ler boolean’dı
O zamanlar stack, heap, fonksiyon çağrısı gibi kavramlar hakkında hiçbir fikrim yoktu
Bunu yapmak ve bakımını/debug’ını yürütmek için ne kadar enerji harcandığını hayal bile edemiyorum
Yanlış hatırlamıyorsam ZX81’de BASIC programları yazarken “stack yok”a yakın bir yöntemle yazıyorduk
1 GOTO 3010 LET C = A + B20 RETURN30 LET A = 140 LET B = 250 GOSUB 1060 LET A = C70 LET B = 380 GOSUB 1090 PRINT CRUN6Yazıda derleyicinin yaptığı işi bizzat yapıyordum yani. Satır numaraları bellek adresleriydi, gizli değişkenler de benden gizli değildi. Çünkü derleyici bendim
Yorumlayıcının yaptığı tek şey
GOSUB’ın dönüş adresini saklamaktıYine de kod sözdizimsel olarak yanlış olabilir ya da belleğim çarpıtıyor olabilir. 40 yıl uzun süre, ama genel fikir doğru
Ayrıca makinenin içindeki Z80 işlemcide stack yönetim özelliği vardı. BASIC yorumlayıcısı gerçekten çok basitti ama mazereti de vardı: yalnızca 1KB RAM ve OS, yorumlayıcı, her şeyi barındıran 8KB ROM
GOSUB,RETURN’ün başvuracağı satır numarasını ya da başka bir referansı saklar;GOSUBçağrılarını iç içe yaptığınızda birden çok dönüş noktasını hatırlaması gerektiği için bir biçimde stack gerekirAncak bazı BASIC’lerde genel amaçlı stack yerine yalnızca dönüş pointer’larının sabit bir dizisi ve mevcut konum indeksi vardı; örneğin çağrı derinliği 7 ile sabitlenmiş olurdu. Programcı açısından çağrı stack’i gibi davranır
Elbette birinin stack dediğinde bekleyebileceği yerel değişkenler/parametreler içeren “düzgün” bir stack değil
BBC BASIC’in varsayılan ortamında iç içe çağrılarda, recursion dahil, neler olduğunu gösteren eğlenceli bir demo yapılabiliyordu. Stack konumunu görüntü belleğinin en üstüne alıp oraya bir şey çizilmemesini sağlarsanız, işlem ilerledikçe stack’in büyüdüğünü görebilirdiniz
Ekran çözünürlüğü düşük olduğundan 2 baytlık dönüş adresi ekran modu 1 veya 5’te 8 kalın piksel olarak görünürdü. Mod 2’de 4 taneydi ama yanıp sönen renk olduğundan daha kötüydü; mod 0, 3, 4, 6’da ise 16 taneydi, fakat bit düzeyinde bakmak 8 renk tekrarını seçmekten daha zordu
Keyfi şekilde genişletilebilen heap’ler olmadan önce programcılar en azından biraz mühendislik muhakemesi yapardı
Çünkü girdilerin olasılıksal dağılımını hesaba katmaları ve tüm ara depolama alanlarının boyutunu uygun ayarlamaları gerekirdi
Bu yüzden “BUGS AND LIMITATIONS” ortaya çıktı
Bu yüzden her şey derleme zamanında statik olarak ayrılır ve girdinin ne kadar bellek tüketeceğini bilmeniz gerekir
Ama bellek tüketiminin üst sınırını bilmek, eskiden uygulama programcıları için de normal bir şeydi. Çünkü bellek yetersizliğini asla istemezsiniz
Günümüzde bellek kullanımını öylece YOLO’ya mı bırakıyoruz diye düşünüyorum
Örneğin sed’in maksimum komut uzunluğunun sonlu ve kısa olması gibi sınırlamalara kıyasla büyük bir iyileştirmeydi
O kadar uzun süredir fonksiyonel programlama yapıyorum ki, özyineleme olmadan nasıl kod yazılacağını gerçekten aklıma getirmekte zorlanıyorum
Özyinelemeli algoritmaları yinelemeli algoritmalara nasıl dönüştüreceğimi teknik olarak biliyorum ve kaynak kısıtlarının büyük olduğu yerlerde bunu yapmışlığım da var, ama sevmiyorum
Genelde özyinelemeli olanın daha güzel olduğunu ve vakaların %99'unda yeterince hızlı olduğunu düşünüyorum. Derleyici kuyruk özyinelemeyi destekliyorsa bu %100'e yaklaşır, ama daha ilginç işlerin çoğunda zaten yığını kendiniz tutmanız gerekir
Bazen, ben doğmadan önce insanların bunu nasıl yaptığını öğrenmek için özellikle böyle işler yapıyorum. Ara sıra Commodore 64 oyunlarıyla uğraşıyorum ve hızlı, ucuz, kullanımı kolay donanıma alışmış olduğumuz bu dönemin ne kadar büyük bir lüks olduğunu ciddi biçimde hissediyorum
Böyle eski makinelerde özyineleme yapmak için yığın mekanizmasını kendiniz oluşturmanız gerekiyordu; yine de global depolama dışında temelde kullanabileceğiniz bir yol olmadığından çözülmesi gereken sorunlar kalıyordu
O dönemleri yaşadım ama kimseye tavsiye etmek istemem
Enhanced GNU Awk'in
@letözelliğinde, fonksiyon dışında, örneğinBEGINveyaENDblokları içindeki@letblokları için derleyicinin gizli global değişkenler ayırmasına izin verilmiştiBu değişkenler bloklar arasında mümkün olduğunca yeniden kullanılır
$ ./gawk --dump-variables 'BEGIN { @let (a, b, c = 1) { } }'$ cat awkvars.out$let0001: untyped variable$let0002: untyped variable$let0003: 1ARGC: 1ARGIND: 0ARGV: array, 1 elementsBINMODE: 0[ .. snip many ]https://www.kylheku.com/cgit/egawk/about/
pingde olmuyor,nc -z 104.37.63.7 443de olmuyorGüncelleme: Güvenlik altyapısı bozulmuş gibi görünüyor. Onun ne olduğunu da bilmiyorum, Twitter da kullanmıyorum. AS'ye bakınca Google Fiber çıkıyor
Ayrıca dox'lanmak istemem