6. svibnja 1999.

Jucerasnje Elinkino predavanje o heap memoriji i pokazivacima u Pascalu pratilo je relativno malo ljudi tako da nekima na listi nece biti bas sasvim jasno zasto pisem ovo sto pisem, ali ne smeta...

HEAP

Goran Paulin

Heap je jedna od onih stvari koje se uvuku u informatiku zato jer se svojevremeno cini dobrom idejom, a onda se toliko iskomplicira teoretiziranjem da uskoro svima postane kristalno jasno o cemu je rijec (obzirom da je objasnjeno na sve moguce nacina na koje se moze objasniti), a sam pojam se zacementira do te mjere da u najmanju ruku ispada kako bez njega ne bi bilo ni danasnje informatike.

Yeah, right.

Prvo sto mi pada na pamet je analogija s imenovanjem varijabli tipa integer slovima "i", "j"... Ovome naizgled nije ovdje mjesto, ali dobar dio nastavnika informatike takvo imenovanje smatra Bozjom zapovjedi. Kako nas matematicka notacija od malih nogu nauci baratanju sa "x", "y" i "z" - eto problema. Tesko je dobiti smislen odgovor zasto se koriste bas "i", "j" i druzina usprkos tome sto je odgovor zapravo jednoznacan - Fortranova konvencija. Ako varijabli nije specificiran tip (integer ili real), ona po defaultu postaju integer ukoliko joj je prvo slovo u rasponu od "i" do "n". Sve drugo je real. Dakle, kad se u skoli bude (ponovo) ucio Fortran na vama je da se ovoga i drzite. Dok god prve korake radite u BASICu varijable mozete nazivati i "gefufna". O mnemonicima i razumijevanju sadrzaja varijabli prema imenu - nekom drugom prilikom.

Kakve veze ovo ima s heapom? I jedno i drugo se uzima "as is". O tome se ne raspravlja.

Za bolje razumijevanje problematike kljucan je trenutak do kojeg smo jucer dosli u trenutku kad je Damir konkretizirao problem uzaludnosti koristenja heapa, na kojem sam inzistirao, spomenuvsi dvije stvari - "Pascal" i "640K".

Racunala nisu nikad cula za heap. Mozda su i cula, ali im ne znaci puno u zivotu. Culi su za pojam memorije, za cache i, u najboljem slucaju, za stack. To znaci da je CPU naseg vremena u stanju samostalno razlikovati ta tri termina. Ne treba mu nikakv Wirth koji ce ga uvjeravati da tu postoji jos gomila stvari kojih nije ni svjestan.

Cache nas ovom prilikom ne zanima obzirom da ni stack ni heap nemaju puno veze s njim u daljnjem kontekstu.

Govoreci o stacku dodirujemo jedinu konkretnu granicu konvencionalne memorije. Onu kojoj je Motorola odlucila posvetiti osmi adresni registar (a7) svojih CISC procesora. Spominjem Motorolu jer mi je daleko bliza od Intela. Ljubitelji Intela, slobodno se nadovezite.

Dakle, tipican procesor serije MC680x0 svjestan je postojanja stacka i obilato ga koristi. Postojanja heapa nije svjestan. Na malo visem nivou, tamo gdje jedan od likova nase bajke postaje i sam OS, stack dobiva novo znacenje - o njemu se brine OS. Stack je elementarno jednostavan dok god se radi o OSu koji nije cuo za multitasking. Onaj koji ima prefix "RT" (kao real-time) stack tretira puno dinamicnije.

Ni OS nije cuo za heap. Pod uvjetom da ne zelimo "lupati po metalu" i zamarati se mnemonicima, koristit cemo neki programski jezik kao sto je, gle, gle - Pascal. Wirth se nikada nije uspio dogovoriti sam sa sobom kome zeli koga podrediti - racunalo korisniku, korisnika OSu, OS racunalu, OS kompajleru, korisnika kompajleru... zato je tijekom godina eksperimentirao i napisao Pascal, Modulu, Oberon... da spomenemo samo nesto iz njegove radionice. Svaki put se sve vise priblizio onome sto danas nazivamo objektno programiranje, a vec sa Pascalom odlucio je potuci legendarni quote Billa Gatesa kako bi "640K trebalo biti dovoljno za sve".

Wirth je znao da nije. U biti, znao je i Gates. Jedan je uveo virtualnu memoriju, drugi heap. Virtualna memorija je prilicno samoobjasnjiva. To je memorija koju zapravo nemamo. Heap nije daleko od toga, ali zapravo je rijec o nacinu na koji se Pascal brine za ono za sto bi se inace morali pobrinuti sami.

Ovdje mozemo vuci paralele s vizualnim alatima i nacinima na koji se brinu za GUI i interakciju s istim. S heapom radimo isto to - s memorijom.

Ako u prostor od 21 memorijske jedinice moramo strpati 42 podatka od po jedne memorijske jedinice, ne moramo se pitati hoce li "stati". Nece, naravno, barem ne svi istovremeno, ali ce ih heap preuzeti onoliko koliko mu Pascal dozvoli, obraditi ih i proslijediti dalje. Naravno, uvijek ostaje kljucni problem u kojem program ne smije previse sarati po memoriji u smislu "sad cu obraditi jedan podatak na lokaciji 1, a onda idem malo na 38 pa cu se vratiti na 2". Istina, radilo bi i tako, ali otprilike 21 puta sporije.

Heapu nije puno trebalo da se useli u sve vise programske jezike, a Jobs i njegova vizija MacOSa posluzila se preventivnim alokacijama memorije u kojoj se nekako naslo i mjesta za heap. Obzirom da, koliko mi je poznato, ovdje nitko ne koristi Maca, a jos manje programira na njemu, to cemo preskociti.

Problem heapa? Problem lezi u tome sto kompajler mora pretpostaviti sto vi zapravo hocete. Obzirom da kompajleri ne citaju misli, heap vise uzima nego sto daje. Naime, on savrseno funkcionira ako rjesavamo zadatak u Pascalu i ako nismo u zivotu alocirali memoriju. Medjutim, koliko puta mislite da cete imati problem alokacije, primjerice, polja velicine 1000 integera kojeg cete rijetko kad popuniti preko polovice? Tu suvereno vlada heap.

U stvarnom zivotu ne vrijedi pretpostavljati. Intuicija, kao sto se moglo vidjeti iz traktata o dinamickom programiranju, nema puno zajednickog s racunalima. Doista mi ne znaci puno ako rezerviram 5 polja za integer ako nisam siguran hoce li korisnik upisati nesto sto stane u tri ili cetiri polja. Naravno, glupo je uvijek pitati korisnika koliko on zeli alocirati memoriju jer vjerojatno ne zna ni sto ga pitamo, pogotovo ako s druge strane ekrana sjedi Stefica iz urudzbenog.

U takvim situacijama potrebno je svladati onaj dinamicki dio u programiranju koji nema veze sa samim dinamickim programiranjem, a odnosi se, izmedju ostalog, na dinamicko koristenje memorije. U doba vladavine MS-DOSa to nije bilo nesto na cemu su se lomila koplja, ali svaki multitasking OS to ima zapisano kao imperativ. Zamislite da N malih programcica negdje nesto radi i svaki od njih rezervira "za svaki slucaj" 640K za nekakav heap. Tajvanci bi procvali od srece.

Tipican primjer problematike alokacije memorije i izbjegavanja heapa je ucitavanje datoteke. Datoteka je velicine N bytova i nema nacina na koji mozemo preventivno procjeniti njenu velicinu te alocirati nekekav prostor u memoriji koji ce zauvijek biti dostatan. Tu pada u vodu teorija po kojoj program svoje izvrsavanje zapocinje alociranjem mjesta za varijable (to su one iste varijable koje pisu na pocetku programa "da ih lakse nadje"). Automatsko alociranje gubi smisao iz dva razloga - prvi je taj sto ne znamo velicinu pa mozemo samo nagadjati (makar to bio i educated guess) i nailaziti na prekinuti program jer podataka ima previse ili pak na neracionalno iskoristenu memoriju koja zjapi prazna, uredno alocirana. Drugi je ono sto se naziva "memory pooling", a tice se fragmentiranja memorije kao najnepozeljnijeg nacina rukovanja istom (cak i kad se za nju brine MMU).

Ovo drugo bi nas mozda i zadovoljilo, ali...

Rjesenje je u promjeni tijeka samog programa. Necemo alocirati nikakav prostor za podatke. Zatrazit cemo od DOSa (u smislu necega sto se brine o file-systemu pa nam kao takvo moze vratiti i podatak o velicini datoteke) tu informaciju i pokusati alocirati potrebnu kolicinu memorije. OS nam u vecini slucajeva moze dati dva podatka vezana uz memoriju - ukupnu kolicinu raspolozive memorije i kolicinu najveceg bloka. Problem - alocirati mozemo samo vrijednost unutar najveceg bloka. Drugim rijecima - ako imamo 100 MB memorije koju je necije "pametno" rjesenje iz prethodnog odlomka fragmentiralo tako da sad imamo 100 blokova po 1M i nikakav MMU koji ce to pocistiti za nama (MMU kao memory managment unit, da ne bude "nisam znao"), eto nam problema - ne mozemo alocirati vise od 1M. Imamo memoriju, a nemamo memoriju.

Znaci, sad treba "pjeske" obaviti ono za sto bi se u nekim idelanim uvjetima pobrinuo heap. Zatrazimo od OSa podatak o LMBu (largest memory block) i alociramo ga (ako nam je potrebno bas toliko ili vise). Pametno programiranje na vrlo niskom nivou ne bi zatrazilo LMB nego memory pool koji je velicinom najblizi nasim zahtjevima.

Kad alociramo potrebnu memoriju ili njezin dio, provjerit cemo ima li OS na raspolaganju jos memorije za nas program. Ako nema, morat cemo podatke obraditi unutar kolicine koju nam je dodijelio. Lameri ce ispisati "Not enough memory" i srusiti system da se ne zamaraju.

Ako ima, mozemo se posluziti vezanim listama. Obzirom da ih je Elinka uredno objasnila, necu ponavljati sto su i zasto su, ali to ujedno zaokruzuje pricu o pokazivacima. Naime, kad se varijable inicijaliziraju tijekom izvrsenja programa (dakle, kad su napisane "na vrhu" koda i konkretno definirane), za njih se u memoriji vrsi alokacija putem onog dijela koda koji kompajler priljepi nasem programu kad ga prevodi u exe. U slucaju dinamickih alokacija, ne mozemo znati gdje se nalazi varijabla "i" alocirana naknadno. Razlozi leze u samom nacinu prevodjenja koji takodjer nema smisla sad objasnjavati, ali tice se relokatibilnosti koda i nacina na koji se CPU brine za interpretiranje pojedinih instrukcija. Jedina informacija koju nam alociranje vraca je adresa na kojoj se nalazi alocirani blok. Ako je u pitanju pametniji OS koji na raspolaganju ima i MMU, taj podatak se prosljedjuje i njemu kako bi se mogao preuzeti posao cistaca bazena odnosno onoga sto, primjerice, Javi na daleko visem nivou radi garbage collector.

Rezime - prvo nam nedostaje memorije, potom taj problem pokusavamo rjesiti koristeci automatiku heap memorije i automatizirano upravljanje njome. Nakon toga budimo se u stvarnom zivotu i nailazimo na programiranje multitasking OSa gdje heap jos uvijek radi, ali nam nedostaju terbajati memorije koliko si preventivno prisvaja. Tada upoznajemo alternativu - dinamicku alokaciju memorije. Napustamo automatiku, a pametni kompajler ce nam omoguciti iskljucenje heapa i prepustiti nam da se sami brinemo za memoriju. Tu negdje upoznajemo konkretnu vrijednost pointera i pokusavamo zaboraviti na blagodati GOTO naredbe. Kakve veze ima GOTO? Ono sto vraca alocirana memorija je konkretna adresa memorijskog prostora na koji, gle, gle, mozemo i skociti iz naseg programa! Dakle, otvara se prostor za skakace koji se obicno koristi iskljucivo za dinamicku relokaciju koda (hint: virusi). U praksi ne zelimo skakutati po memoriji jer ne znamo gdje ce nas odvesti i ne zelimo alocirati memoriju na konkretnoj memorijskog lokaciji (iako mozemo) samo zato sto smo u programu napisati da se podatak procita s adrese $00700000. Ovo je ekvivalent skakutanju s GOTO naredbom, naziva se "lupanje po metalu" i potice iz starih dobrih vremena kad su pravi momci nocima pisali introe i silom pokusavali ne samo OSu nego i procesoru oduzeti kontrolu kako bi ustedili memorijski ciklus ili dva.

Skakace su u C-u preduhitrili pokazivacima. Naravno, ostavili su im GOTO naredbu ne bi li se programeri "naviknuli". Khm. Zanimljivo da je i Java tretira kao kljucnu rijec, ali bez konkretne uporabe. Spominjem javu jer je otisla jos dalje zabranivsi manipulaciju putem pokazivaca. Ako mislite da je vizualno razvijanje aplikacija pametno, mozete se sloziti i s uskracivanjem koristenja pokazivaca obzirom da u praksi cesce dovode do pogreske kobne po OS od GOTO zavrzlama!

Pokazivaci u Pascalu nisu (smisleno) najsretnije rjeseni. C-ov pristup je puno prihvatljiviji (proucite pa odlucite sami), a za najlakse razumijevanje uvijek je dobro spustiti se do dna i vidjeti sto o tome misli CPU. Ovdje opet vrijedi spomenuti Motorolu koja programere odmah nauci na pokazivace. Naime, za razliku od Intela, motorola razlikuje samo dva tipa registra - adresne (komada osam) i registre za podatke (takodjer komada osam). U tom smislu oni za podatke (imenovani od d0-d7) sadrze konkretan podatak koji stane u 32-bita, dok adresni (a0-a7) sadrze adresu (takodjer 32-bitnu). Kvaka je u tome sto su adresni zapravo pokazivaci cija se vrijednost (tijekom kodiranja) moze citati jednostavnim stavljanjem imena registra u zagrade cime na neki nacin postaju ekvivalenti data registrima.

Na taj nacin moguce je vrlo brzo pohvatiti konce i logiku koristenja pokazivaca obzirom da se kompletno asemblersko programiranje bazira na njima.

Evo, necu dalje. Pokusajte s vremena na vrijeme stvari stavljati u kontekst i uciti iz onoga sto vec znate. Ovdje bi se sad fino moglo nastaviti s pricom o samim pokazivacima i njihovoj primjenjivosti, ali to cemo pustiti za neki drugi dan.

(c) 06/05/1999 Goran Paulin