Alokace haldy
                                -=-=-=-=-=-=-=-

    "Real programs do not generally behave randomly - they are designed to
                            solve actual problems."

       -- Wilson, Johnstone, Neeley, Boles: Dynamic Storage Allocation:
                         A Survey and Critical Review

          Každý asi zná skupinu  funkcí  malloc,  calloc,  realloc a free.
      Proto  používání  zde  nebudu   rozebírat.   Raději  se  porozhlédnu
      po možnostnech jejich implementace.

          Jak  jsem  už  říkal,   klasická   implementace   zpravuje  data
      v souvislé  oblasti  pod  ukazatelem  break a sama  se  stará o jeho
      posouvání. Asi největším  problémem je fragmentace paměti. Například
      pokud program naalokuje 4MB blok, potom 2 bajty a první blok uvolní,
      pokud   knihovna   umístila   bloky  v paměti  ve  stejném   pořadí,
      nemůže  ukazatel break vrátit,  protože je blokován tím malým blokem
      na konci.  Vzniká  tak  fragmentace  paměti a stím se musí  knihovna
      nějak vyrovnat.

          Bežně se  používají  dva  základní  přístupy. V prvním  přístupu
      (použitým  například v glibc)  si  knihovna  udržuje  bloky ve  dvou
      spojových  seznamech  pro  volné a  použité  bloky.  Pokud  se  blok
      alokuje,  knihovna  se pokusí  najít  první  dostatečně  velký  blok
      v  prvním  senzamu  a  užízně  z něho  potřebnou  část  (splitting).
      Pokud se blok uvolňuje,  knihovna  přemístí  blok z jednoho  seznamu
      do druhého a opět jej sloučí s okolními volnými bloky (coalesting).

          Problémem této implementace je výběr volného bloku. Je jasné, že
      z důvodů rychlosti  knihovna musí udržovat nějakou chytřejší datovou
      strukturu  pro volné  bloky,  než je seznam.  Navíc  pokud  knihovna
      vybírá  první dostatečně velký  blok,  počíná si poměrně  hloupě. Je
      jasné, že by šlo použít chytřejší metody. Pokud ale program  používá
      nejmenší  větší  blok, má tendenci  paměť  rozfragmentovat do malých
      nepoužitelných  částí.  Pokud  naopak  používá  největší,  může  jej
      znehodnotit pro další alokace. Je tedy poměrně těžké zvolit  rozumný
      algoritmus.  Kupodivu i pro první důvod - alokovat první dostupný je
      dobrý důvod. Objekty  alokované ve stejný čas budou za sebou. Je ale
      i pravděpodobné, že program po zkončení dané činnosti  všechny  tyto
      objekty  uvolní. To je důvod proč tento  hloupý  algoritmus se chová
      překvapivě dobře.

          Druhá implementace využívá bloky jedině velikosti  mocniny dvou.
      Paměť  rozdělí na stránky. U každé  stránky ví, bloky jaké velikosti
      obsahuje. Při alokaci tedy napřed  velikost  zaokrouhlí na nejbližší
      mocninu  dvou a použije se  příslušná  stránka.  Pokud  žádná  není,
      vytvoří se nová.

          Jakkoliv tato implementace zní marnotratně, není to tak strašné.
      Pokud  jsou  bloky   dostatečně   malé  (tato   metoda  se  většinou
      používá  pouze  pro  malé  bloky),  blíží se zaokrouhlení na mocninu
      dvou  paměti  přidané  v  prvním   algoritmu  pro  zarovnání  bloku.
      Navíc stránka  potřebuje  pouze malou  hlavičku a bitmapu pro bloky.
      Datové  struktury jsou mnohem  menší.  Zaokrouhlování  způsobuje to,
      že mnohem  častěji je  díra po jedné  alokaci  použitelná  na jinou.
      Jiný důvod  mluvící pro tuto metodu je, že objety  stejné  velikosti
      jsou většinou objety stejného typu a používají se ke stejnému účelu.
      Je tedy pravděpodobné, že také budou naráz uvolněny.  Režije je tedy
      v mnoha  případech  dokonce  menší, než u prvního  algoritmu.  Tento
      postup používá například klasická UNIXová knihovna.

          Jak   vidíte,    nejedná   se  zdaleka  o  triviální    problém.
      Každá  implementace  používá   jakousi  svoji  strategii   odvozenou
      z programátorských  zkušeností  jejich  autorů a proto se chová lépe
      či hůře v různých  případech.  Neexistuje žádná  nejlepší  strategie
      a na každou je nějaký  protipříklad. Tyto problémy jsou velmi  pěkně
      a podrobně rozebrány ve článku [Wilson1].

          Popsané metody se většinou  používají pro alokace bloků menších,
      než je velikost  stránky  (4KB).  Pro  větší  bloky  knihovny  často
      používají  volání  mmap a alokují  je  uprostřed  díry  mezi  haldou
      a zásobníkem.  Tento  přístup je sice  pomalejší  (pokaždé  vyžaduje
      volání OS), ale zabrání se tím probémům při vracení paměti  systému.
      U těchto bloků tedy problémy s fragmentací  odpadají.  Zůstává pouze
      promrhána paměť mezi koncem  bloku a koncem  stránky (tedy méně, než
      4KB).

          V DJGPP  je  situace  ještě  komplikovanější.  DPMI  má  poněkud
      odlišnější  přístup  ke  správě  paměti  a  alokuje  souvislé  bloky
      ve fyzické  ram. Bežně  knihovna pro každý dový blok  alokuje i nový
      blok v DPMI.  Tento  přístup  ale  má  za  následek,  že v některých
      implementacích, kde je počet bloků omezen, program nemůže naalokovat
      celou  paměť.  Druhá  implementace  je, že  knihovna  mění  velikost
      jednoho bloku. Potom se halda chová skoro stejně jako v UNIXu. Často
      ale může  dojít k přesuvu  bloku v RAM (a tedy  překopírování  všech
      dat) a navíc některé implementace  nepovolují naalokovat blok větší,
      než je polovina volné paměti.

                                    Mallopt

          Pro  nastavení  parametrů  alokačního  algoritmu  slouží  funkce
      mallopt. Tato funkce odpovídá standardu SVID/XPG. Umožňuje nastavení
      následujících hodnot:

      M_TRIM_THRESHOLD  určuje  minimální  velikost  horního  bloku, která
         způsobí posunutí  ukazatele break zpět (na konci haldy se používá
         malá cache, která má za úkol ušetřit volání sbrk).

      M_TOP_PAD  určuje  množství  paměti,  která se naalokuje  navíc  při
         pousouvání ukazatele break.

      M_MMAP_THRESHOLD minimální velikost bloku, pro který se použíje mmap
         místo haldy.

      M_MMAP_MAX  maximální  počet  bloků, které se alokují  pomocí  mmap.
         Nastavení na 0 vypne používání mmap úplně.

                                   Mallinfo

          Další funkce  definována  standardem  SVID/XPG je mallinfo. Tato
      funkce  vrátí strukturu  mallinfo,  která  obsahuje mnoho  informací
      o haldě (kolik bloků je alokováno, kolik paměti, fragmentaci apod.).
      Ve verzi glibc, která byla v distribuci  RedHat 5.0 ale ze záhadných
      důvodů haldu nějakým způsobem rozhodí a další alokace paměti selže.

                          Alokování zarovnaných bloků

          Někdy je potřeba,  aby  byl blok  zarovnán na adresu  dělitelnou
      nějakým   číslem.  BSD  za  tímto  účelem  přidává  funkce  memalign
      a valloc. U funkce  memalign  můžete přímo udat na jakou  hodnotu se
      zarovnává a valloc  zarovnává  na velikost  stránky. V glibc  můžete
      bloky uvolnit  pomocí  funkce free. V BSD žádný  způsob  neexistuje.
      Mimochodem těmto funkcím chybí manuálová  stránka a najdete je pouze
      v texinfo dokumentaci.

                                GNU extensions

          GNU přidává ještě jednu zajímavou  funkci-mcheck, která umožňuje
      zkontrolovat, jestli je halda vpořádku. Tato funkce si nastaví svoje
      náhražky  za  malloc/realloc/free a kontroluje  chyby  jako je zápis
      za koncem  bloku a další.  Kontrola  se musí  zaktivovat  před  tím,
      než cokoliv naalokujete. Jinak funkce neudělá nic. Asi nejjednodušší
      metoda, jak toho docílit, je přilinkovat knihovnu mcheck (-lmcheck).
      Pro  kontrolu  paměti ale existují  mnohem  robusnější  prostředky o
      kterých si něco řekneme později.

          Navíc knihovna  umožňuje  předefinovat  ukazatele  __malloc_hook
      a další. Ty umožňují  nadefinovat  vlastní  funkce  pro malloc/free.
      (například verzi, která zkončí v případě, že není paměť apod.)

                             Relocating allocator

          Metodou  jak  účině  zabránit  fragmentaci je  ponechat  zprávci
      paměti  možnost  bloky  libovolně  přesouvat.  Tento způsob asi bude
      důvěrně znám  programátorům z Macintoshe, kde je tato  metoda  běžně
      používána. Při alokaci  paměti  nedostanete  přímo  ukazatel na nový
      blok,  ale  ukazatel na ukazatel  (handle) na blok.  Správce  paměti
      má potom  možnost  tento  ukazatel  libovolně  měnit a pokud program
      k přistupu do bloku důsledně  používá tento nepřímý  odkaz, vše bude
      fungovat dobře.

          Toto  řešení ale má mnohá  úskalí.  Napsat  program  tak, aby se
      ukazatel  mohl  změnit  kdykoliv je téměř  nemožné.  Žádné  knihovní
      funkce  jazyka  C  nejsou  navrženy  tak,  aby  pracovaly   korektně
      v případě, že se blok přesune.  Navíc by všechny tyto odkazy  musely
      být typu volatile a to by vedlo k velmi  špatnému  výslednému  kódu.
      Proto se většinou  možnost  přesouvání  bloků musí omezit na nějakou
      speciální  akci  (například   volání   funkcí  pro  zprávu   paměti)
      a proto se tato metoda  vpodstatě  nedá  použít v multithreadovaných
      programech, nebo programech co alokují paměť ze signálů.

          Z těchto důvodů  osobně  nevidím žádně větší využití pro takovou
      zprávu  paměti.  Přesto ale glibc  implementuje  několik  funkcí pro
      práci s takovými bloky:

      void * r_alloc (void **HANDLEPTR, size_t SIZE)
      void r_alloc_free (void **HANDLEPTR)
      void * r_re_alloc (void **HANDLEPTR, size_t SIZE)

          Jejich  použití je asi  jasné a proto je zde  nebudu  rozebírat.
      Jenom je  nutné  poznamenat,  že handle se  musí  nacházet v paměti,
      která  se  nebude  přesouvat a nemůže  být  tedy v bloku  alokovaném
      pomocí r_alloc.

                                   Obstacks

          Halda je rozhodně  nejuniverzálnějším  prostředkem  pro  alokaci
      paměti. Za  její  univerzálnost  se ale  platí  její  marnotratností
      a pomalostí.  Jeden z nejklasičtějších  případů, pro které se nehodí
      je načítání neomezeně dlouhých  stringů. Vpodstatě se nám nabízí dvě
      základní řešení:

      1) můžeme  volat  realloc po každém  načteném  byte. Toto  řešení je
         ale velmi pomalé a protože  realloc může pokaždé  přesunout blok,
         v nejhorším případě je k načtení proběhne v kvadratickém čase.

      2) teoreticky asi  nejelegantnějším  způsobem by asy bylo  prohlásit
         každý znak za objekt a načítat tyto objekty do spojového seznamu.
         Nevýhodou  tohoto  řešení  je  to,  že  hlavičky v seznamu  spolu
         z režijí  zprávce  haldy by zabíraly  několikanásobně víc, než je
         vlastní  string.  Navíc by se  potom z načteným  stringem  velice
         špatně pracovalo.

          Je  jasné, že ani  jedno z těchto  řešení  není  správné.  První
      řešení  lze  vylepšit  tak, že budeme  paměť  reallokovat po větších
      blocích.  Tím se  načítání  výrazně  zryhlí,  ale  nezmění se časová
      složitost.  Podobně v druhé  metodě  můžeme do jednoho prvku seznamu
      načíst  větší množství  znaků a vytvořit tak seznam  stránek. I toto
      řešení má své  problémy  zejména  při  náhodném  přístupu k načteným
      znakům. To sice lze vyřešit  adresářem  stránek ale výsledné  řešení
      je  už  relativně  komplikované a běžné  knihovní  funkce z načteným
      stringem  nebudou fungovat. Proto většina programátorů stejně raději
      použije  pole  pevné  velikosti.  Výsledkem  jsou pak problémy  typu
      buffer overflow.

          Situace by byla mnohem  jednodušší,  kdybychom  měli k dispozici
      zásobník. Znaky by se prostě  načetly do zásobníku a nakonec by byly
      pěkně za sebou stejně jako u fixního pole.  Nevznila by žádná  dalši
      režije.  Problémem  je, že klasický  zásobník je už použit  pro jiné
      účely a proto  se k němu  nedostaneme.  Navíc v připadě,  že  bychom
      načítali více stringů zároveň potřebovali bychom i více zásobníků.

          Práve o to se stará  konstrukce  obstack. Ta  umožňuje  vytvořit
      libovolné  množství  zásobníků  obsahující objekty libovolného typu.
      Sice  nakonec   stejně  použije  haldu,  ale  už  před  námi  skryje
      nepříjemné implementační  detaily. Navíc v glibc exitují ekvivalenty
      některých  funkcí jako printf  pracující z obstacky a proto je práce
      s nimi relativně příjemná.

          Pokud chcete obstacks vyžít, je nutné napřed  zásobník  vytvořit
      a zinicializovat asi následujícím kódem:

     #include <obstack.h>
     ...
     static struct obstack myobstack;
     ...
     obstack_init (&myobstack);

          Pro práci s obstacky  existuje několik funkcí. Ty jsou v případě
      GNU C implementovány jako  inline  funkce.  Pokud  používáte  starší
      překladače,  které  nemají  inline  funkce, jsou implementovány jako
      makra. Proto je nutné dbát na to, aby parametry těchto funkcí neměly
      vedlejší efekt.

          V případě, že je obstack  inicializován, asi budete chtít do něj
      něco uložit. Na obstack můžete postupně  ukládat  objekty  libovolné
      velikosti.  Můžete na něm alokovat bloky podobně, jako pomocí malloc
      na haldě:

      void * obstack_alloc (struct obstack *OBSTACK-PTR, int SIZE)

          A potom opět uvolňovat pomocí obstack_free:

      void obstack_free (struct obstack *OBSTACK-PTR, void *OBJECT)

          Můžete ale uvolňovat vždy jen blok na konci zásobníku.

          Nejzajímavější  vlastností  je ale  načítání  objektů, u kterých
      nevíte  přesně  délku.  Ty můžete  načítat po částech a každou  část
      "přilepit" pomocí funkce:

      void obstack_grow (struct obstack *OBSTACK-PTR, void *DATA, int SIZE)

          Když  je  obstack   hotov,   zavoláte   obstack_finish(obstack).
      Uprostřed   načítání  objektu  nemůžete  na  obstack  přidávat  jiné
      objekty, ani z něj něco mazat. Pro práci z obstacky  existuje  ještě
      několik  dalších  funkcí (pro přidávání  jednoho znaku,  superrychlý
      růst  atd.).  Myslím  ale, že nemá smysl to zde rozebírat. V texinfo
      dokumentaci glibc v sekci "Memory allocation" najdete víc.

          Zajímavé je, že s obstackem lze pracovat  také  jako se souborem
      a otevřít  jej pomocí  funkce  open_obstack_stream(&mystream) a dále
      používat  běžné  funkce  pro  práci se souborem  (například i včetně
      fseek,  nebo  fflush).  Přímo  práci s  obstackem  podporuje  funkce
      obstack_printf a některé  další  bohužel  ne  příliš  zdokumentované
      funkce glibc.

                                   Více hald

          V předchozích  odstavcích jsem vysvětlil  výhody více zásobníků.
      I použití více hald přinaší  často  výhody. Ty jsou patrné  zejména,
      pokud  potřebujeme  uložit stav  programu, což velice  často  dělají
      interpretery  lispu a dalších  jazyků. Je potom  možné  vyhradit  si
      jednu  haldy  výlučně na objekty  onoho  jazyka a uložit  pouze  ji.
      Pokud navíc knihovna  zaručí, aby se příště halda nahrála na stejnou
      adresu, jste vlastně hotovi.

          Knihovny pro zprávu více hald se používají  velmi  podobně, jako
      malloc/free pouze s tím rozdílem, že musíte navíc udat identifikátor
      haldy, proto jejich použití nebudu více rozebírat.  Standardně glibc
      tuto  vymoženost  neimplementuje a proto si  musíte  sehnat  nějakou
      další knihovnu. Jednu najdete v programu checker (o kterém se zmíním
      později) a další je v programu gdb.


            výheň