Programovací jazyk Forth a zásobníkové procesory (19)

17. 5. 2005
Doba čtení: 12 minut

Sdílet

Dnešní pokračování seriálu o programovacím jazyku Forth a zásobníkových procesorech bude věnováno popisu třicetidvoubitových zásobníkových procesorů určených pro běh velmi úsporného javovského bytekódu. Tyto procesory měly v představách firmy Sun Microsystems představovat základ pro takzvaný Network computer, tato zajímavá myšlenka se však prozatím ve světě IT příliš neujala.

Obsah

1. Programovací jazyk Java
2. Běhové prostředí Javy a bytekód Javy
3. Instrukce JVM
4. Mikroprocesor picoJava
5. Mikroprocesor microJava
6. Obsah dalšího pokračování

1. Programovací jazyk Java

Programovací jazyk Java patří do rodiny objektově orientovaných jazyků, je relativně jednoduchý (zejména v porovnání s velmi komplikovaným C++), přenositelný, nezávislý na architektuře a v posledních verzích i velmi výkonný – podle některých studií je dokonce program spouštěný v Javě rychlejší než kód přeložený z C++. Java se na první pohled syntakticky podobá programovacímu jazyku C++, ve skutečnosti však má blíže k takovým vývojovým prostředkům, jakými jsou například Smalltalk a programovací jazyk Self.

Java je také od většiny kompilovaných jazyků odlišná tím, že se překlad programu neprovádí přímo do strojového kódu použitého procesoru, ale do speciálně navrženého zásobníkového kódu nazývaného bytekód. Tento bytekód je možné interpretovat na jakémkoli systému, který obsahuje virtuální procesor jazyka Java, takzvaný JVM. Výhodou tohoto řešení je přenositelnost a paradoxně také možnost provádět další optimalizace nad vygenerovaným bytekódem či nad jeho dynamicky detekovanou částí (hot spot).

V první řadě si však musíme uvědomit, že slovem Java a logem Java není (a podle stále platné licence ani nesmí být) označena určitá implementace jazyka ani určitý programový balík – to evidentně nedochází například RMS, který se snaží mimo jiné všude prosadit řešení bez Javy. V případě Javy se jedná, zjednodušeně řečeno, o tři základní specifikace. Specifikace nazvaná Java Language Specification definuje vlastní programovací jazyk, specifikace nazvaná Java 2 Platform API Specification základní třídy, které jsou programátorovi aplikací i samotným aplikacím dostupné, a konečně specifikace Java Virtual Machine Specification určuje parametry prostředí pro běh aplikací (tj. ve skutečnosti virtuální stroj pro bytekód jazyka Java). Vhodnou kombinací a následnou implementací těchto tří specifikací dostáváme do rukou kompletní (i když neinteraktivní) vývojové prostředí pro Java aplikace, takzvané Java Development Kits (JDKs), které obsahují zejména vlastní překladač (compiler), virtuální stroj jazyka Java a základní třídy (core classes) potřebné pro běh aplikací.

2. Běhové prostředí Javy a bytekód Javy

V tomto článku nás bude nejvíce zajímat běhové prostředí, které je určeno pro provozování aplikací napsaných v programovacím jazyce Java. Jak již víme, jedná se o takzvaný JVM (Java Virtual Machine) – virtuální stroj programovacího jazyka a celé platformy Java, který ve své podstatě implementuje idealizovaný zásobníkový procesor a chráněnou paměť, v níž jsou programy napsané v Javě spouštěny. Parametry virtuálního zásobníkového procesoru jsou velmi precizně popsány ve volně dostupné specifikaci Java Virtual Machine Specification, z níž si v dalších odstavcích uvedeme základní informace. Pro bezchybný běh vytvářené aplikace na různých platformách musí být zaručeno, že se JVM bude chovat ve všech případech podle platné specifikace.

Přeložené třídy jsou uloženy v souborech s koncovkou .class. V těchto souborech jsou uložena jak metadata o třídě, tak i vlastní bytekód. JVM musí soubory .class správně rozkódovat a bytekód buď přeložit do výsledného strojového kódu použitého procesoru, nebo tento bytekód interpretovat. V současné době se překlad a interpretace kombinují, což je z hlediska velkých (a dlouhodobě běžících) aplikací pravděpodobně nejlepší řešení, protože se při překladu vybraných částí kódu (hot spots) mohou využít i velmi sofistikované optimalizace.

Abstraktní zásobníkový procesor předpokládá, že se veškeré výpočty budou provádět na zásobníku operandů. Parametry a lokální proměnné je však možné adresovat přímo, což znamená, že jejich počet je omezen (to však při kvalitním softwarovém návrhu nevadí). Výpočty se provádí vždy nad primitivním datovým typem int, long, float nebo double. Hodnoty ostatních primitivních datových typů musí být před provedením operace na jeden z výše uvedených typů převedeny. Navíc existují specializované instrukce, které na zásobník operandů ukládají vybrané konstanty (null, -1, 0, 1), což zkracuje výsledný bytekód a současně i urychluje jeho interpretaci. Podle specifikace JVM by se měly kontrolovat typy hodnot uložených na zásobníku, čímž se alespoň částečně zamezí spuštění modifikovaného kó­du.

3. Instrukce JVM

Všechny instrukce z instrukčního kódu JVM (tj. abstraktního zásobníkového procesoru) mají délku jeden byte a jejich formát je vertikální, na rozdíl od instrukcí zásobníkových procesorů popsaných v předchozích částech tohoto seriálu ([16], [17] a [18]). To znamená, že počet instrukcí je omezen na 256, v současné době je však využito pouze cca 200 instrukčních kódů. Některé instrukce existují i v takzvané rychlé variantě, při které nejsou požadovány některé časově náročnější kontroly. Výhodou takto konstruovaného bytekódu je jeho stručnost, .class soubory (s uloženými informacemi pro ladění) jsou v průměru dokonce menší než původní zdrojový kód. V následujícím přehledu jsou stručně vypsány všechny základní instrukce (bez rychlých variant), které musí každá JVM akceptovat. V prvním sloupci je v desítkové soustavě zapsán jednobytový kód instrukce, ve druhém sloupci její jméno (takto se instrukce zobrazuje v debuggeru) a třetí sloupec obsahuje přepis do pseudoassembleru, případně poznámku.

000  nop              nop
001  aconst_null      push null
002  iconst_m1        push -1
003  iconst_0         push 0
004  iconst_1         push 1
005  iconst_2         push 2
006  iconst_3         push 3
007  iconst_4         push 4
008  iconst_5         push 5
009  lconst_0         push 0L
010  lconst_1         push 1L
011  fconst_0         push 0f
012  fconst_1         push 1f

013  fconst_2         push 2f
014  dconst_0         push 0d
015  dconst_1         push 1d
016  bipush           push const8 (jako 32bit integer, tj. provádí se konverze)
017  sipush           push const16 (jako 32bit integer, tj. provádí se konverze)
018  ldc1             push index32[i8]
019  ldc2             push index32[i16]
020  ldc2w            push index64[i16]
021  iload            push int    var[index8]
022  lload            push long   var[index8]
023  fload            push float  var[index8]
024  dload            push double var[index8]
025  aload            push handle var[index8]
026  iload_0          push int    var[0]
027  iload_1          push int    var[1]
028  iload_2          push int    var[2]
029  iload_3          push int    var[3]
030  lload_0          push long   var[0]
031  lload_1          push long   var[1]
032  lload_2          push long   var[2]
033  lload_3          push long   var[3]
034  fload_0          push float  var[0]
035  fload_1          push float  var[1]
036  fload_2          push float  var[2]
037  fload_3          push float  var[3]
038  dload_0          push double var[0]
039  dload_1          push double var[1]
040  dload_2          push double var[2]
041  dload_3          push double var[3]
042  aload_0          push handle var[0]
043  aload_1          push handle var[1]
044  aload_2          push handle var[2]
045  aload_3          push handle var[3]
046  iaload           push int    array32[index32] (array32{handle} a index32 jsou na zásobníku)
047  laload           push long   array32[index32]
048  faload           push float  array32[index32]
049  daload           push double array32[index32]
050  aaload           push handle array32[index32]
051  baload           push byte   array32[index32]
052  caload           push char   array32[index32]
053  saload           push signed array32[index32]
054  istore           pop int     var[index8]
055  lstore           pop long    var[index8]
056  fstore           pop float   var[index8]
057  dstore           pop double  var[index8]
058  astore           pop handle  var[index8]
059  istore_0         pop int     var[0]
060  istore_1         pop int     var[1]
061  istore_2         pop int     var[2]
062  istore_3         pop int     var[3]
063  lstore_0         pop long    var[0]
064  lstore_1         pop long    var[1]
065  lstore_2         pop long    var[2]
066  lstore_3         pop long    var[3]
067  fstore_0         pop float   var[0]
068  fstore_1         pop float   var[1]
069  fstore_2         pop float   var[2]
070  fstore_3         pop float   var[3]
071  dstore_0         pop double  var[0]
072  dstore_1         pop double  var[1]
073  dstore_2         pop double  var[2]
074  dstore_3         pop double  var[3]
075  astore_0         pop handle  var[0]
076  astore_1         pop handle  var[1]
077  astore_2         pop handle  var[2]
078  astore_3         pop handle  var[3]
079  iastore          pop int     array32[index32]
080  lastore          pop long    array32[index32]
081  fastore          pop float   array32[index32]
082  dastore          pop double  array32[index32]
083  aastore          pop handle  array32[index32]
084  bastore          pop byte    array32[index32]
085  castore          pop char    array32[index32]
086  sastore          pop short   array32[index32]
087  pop              pop int (výsledek se ztratí)
088  pop2             pop long (výsledek se ztratí)
089  dup              duplikuje int na vrcholu zásobníku
090  dup_x1           duplikuje int a vrací ho od 3. pozice zásobníku
091  dup_x2           duplikuje int a vrací ho od 4. pozice zásobníku
092  dup2             duplikuje long na stacku
093  dup2_x1          duplikuje long a vrací ho od 3. pozice zásobníku
094  dup2_x2          duplikuje long a vrací ho od 4. pozice zásobníku
095  swap             swapuje dvě položky typu int
096  iadd             add int (operace +)
097  ladd             add long (operace +)
098  fadd             add float (operace +)
099  dadd             add double (operace +)
100  isub             sub int (operace -)
101  lsub             sub long (operace -)
102  fsub             sub float (operace -)
103  dsub             sub double (operace -)
104  imul             mul int (operace *)
105  lmul             mul long (operace *)
106  fmul             mul float (operace *)
107  dmul             mul double (operace *)
108  idiv             div int (operace /)
109  ldiv             div long (operace /)
110  fdiv             div float (operace /)
111  ddiv             div double (operace /)
112  imod             mod int (operace %)
113  lmod             mod long (operace %)
114  fmod             mod float (operace %)
115  dmod             mod double (operace %)
116  ineg             neg int (aritmetická negace)
117  lneg             neg long (aritmetická negace)
118  fneg             neg float (aritmetická negace)
119  dneg             neg double (aritmetická negace)
120  ishl             shl int (aritmetický posun doleva)
121  lshl             shl long (aritmetický posun doleva)
122  ishr             shr int (aritmeticky posun doprava)
123  lshr             shr long (aritmeticky posun doprava)
124  iushr            shr int (bitový posun doleva)
125  lushr            shr long (bitový posun doleva)
126  iand             and int (bitová operace &)
127  land             and long (bitová operace &)
128  ior              or int (bitová operace |)
129  lor              or long (bitová operace |)
130  ixor             xor int (bitová operace ^)
131  lxor             xor long (bitová operace ^)
132  iinc             inkrementuje lokální proměnnou o konstantu
133  i2l              převod int na long
134  i2f              převod int na float
135  i2d              převod int na double
136  l2i              převod long na int
137  l2f              převod long na float
138  l2d              převod long na double
139  f2i              převod float na int
140  f2l              převod float na long
141  f2d              převod float na double
142  d2i              převod double na int
143  d2l              převod double na long
144  d2f              převod double na float
145  int2byte         převod int na byte
146  int2char         převod int na char
147  int2short        převod int na short
148  lcmp             pop a; pop b;push sign(a-b)
149  fcmpl            -//-, vrací -1 pri NaN
150  fcmpg            -//-, vrací +1 pri NaN
151  dcmpl            -//-, vrací -1 pri NaN
152  dcmpg            -//-, vrací +1 pri NaN
153  ifeq             porovnání s nulou a skok při rovnosti
154  ifne             porovnání s nulou a skok při nerovnosti
155  iflt             porovnání s nulou a skok při relaci <
156  ifge             porovnání s nulou a skok při relaci >=
157  ifgt             porovnání s nulou a skok při relaci >
158  ifle             porovnání s nulou a skok při relaci <=
159  if_icmpeq        porovnání dvou operandů a skok při rovnosti
160  if_icmpne        porovnání dvou operandů a skok při nerovnosti
161  if_icmplt        porovnání dvou operandů a skok při relaci <

162  if_icmpge        porovnání dvou operandů a skok při relaci >=
163  if_icmpgt        porovnání dvou operandů a skok při relaci >
164  if_icmple        porovnání dvou operandů a skok při relaci <=
165  if_acmpeq        porovnání dvou polí a skok při rovnosti
166  if_acmpne        porovnání dvou polí a skok při nerovnosti
167  goto             nepodmíněný skok
168  jsr              skok do podprogramu
169  ret              návrat z podprogramu
170  tableswitch      rozeskok dle tabulky
171  lookupswitch      -//- ale existuje default hodnota
172  ireturn          vrací int a následně provede ret
173  lreturn          vrací long a následně provede ret
174  freturn          vrací float a následně provede ret
175  dreturn          vrací double a následně provede ret
176  areturn          vrací handle a následně provede ret
177  return           návrat z podprogramu
178  getstatic        získání statické položky třídy
179  putstatic        nastavení statické položky třídy
180  getfield         získání třídní položky
181  putfield         nastavení třídní položky
182  invokevirtual    zavolání virtuální metody (přes tabulku)
183  invokenonvirtual zavolání nevirtuální metody (přímo)
184  invokestatic     zavolání statické metody
185  invokeinterface  zavolání metody specifikované v rozhraní
186  newfromname      vytvoření nového objektu
187  new              vytvoření nového objektu
188  newarray         vytvoření pole o zadané velikosti
189  anewarray        př. new p[7] se přeloží jako : bipush 7; anewarray <Class "java.lang.p">
190  arraylength      vrací délku pole specifikovaného pomocí handle
191  athrow           throw exception - vyvolání výjimky
192  checkcast        testování typu objektu
193  instanceof       získání instance objektu
194  monitorenter     enter monitored code - použito u multithreadingu
195  monitorexit      exit monitored code - použito u multithreadingu
196  verifystack      dotaz, zda není zásobník prázdný
197  breakpoint       zavolání obsluhy breakpointu
198  multianewarray   vytvoří multidimenzionální pole 

4. Mikroprocesor picoJava

Vývoj mikroprocesoru picoJava začal v roce 1996 ve firmě Sun Microsystems. Tento mikroprocesor byl určen především pro vestavěné systémy, například pro chytré telefony, mikrovlnné trouby, internetové aplikace, TV set boxy a v neposlední řadě i pro takzvaný Network computer, což je ve své podstatě chytrý grafický terminál bez periferních zařízení, který umí lokálně zpracovat bytekód určený pro JVM. Z tohoto důvodu obsahuje picoJava takový instrukční soubor, který do značné míry odpovídá JVM, samozřejmě je však možné použít i aplikace napsané v jiných programovacích jazycích, například v C či C++. Podle dostupných měření je nativně prováděný kód cca 20× rychlejší než jeho interpretovaná varianta běžící na stejném mikroprocesoru.

Návrh tohoto mikroprocesoru (resp. jeho výkonného jádra, které je možné licencovat) byl vytvořen v jazyku Verilog Register Transfer Language (RTL), přičemž celý zdrojový kód měl délku cca 2,4 MB. Interně je na mikroprocesoru vytvořen zásobník, který je sestaven z 64 registrů šířky 32 bitů, které dohromady tvoří kruhový buffer. To znamená, že po uložení 65 hodnot na zásobník se začíná přepisovat první uložená položka – tento princip práce se zásobníkem je sice velmi jednoduchý, na druhou stranu však může způsobit chyby při běhu aplikací.

V jednom taktu je možné provést dvě čtení ze zásobníku a jeden zápis. Vzhledem k tomu, že u firmy Sun přesně nepochopili princip zásobníkových procesorů (například horizontální formát instrukcí), tvoří v reálných aplikacích průměrně celou jednu třetinu instrukcí instrukce pro práci se zásobníkem. Z tohoto důvodu bylo nutné implementovat takzvané skládání instrukcí (instruction folding), které je použito zejména při operacích pro ukládání dat na zásobník – push.

Tento mikroprocesor také částečně podporuje garbage collector. Jádro obsahuje jednotku pro provádění operací v pohyblivé řádové čárce (tu lze pro některé méně náročné aplikace odebrat), dále frontu instrukcí, vyrovnávací paměť pro data i pro instrukce, jednotku pro manipulaci se zásobníkem operandů a částečně i podporu výjimek. Některé složitější instrukce se provádějí pomocí přerušení – v případě, že se narazí na neznámý kód instrukce, provede se přerušení, ve kterém se tento kód odsimuluje.

5. Mikroprocesor microJava

Mikroprocesor microJava byl firmou Sun Microsystems představen 15. 10. 1997. Jednalo se o typ 701 z takzvané série 700. Tento mikroprocesor je taktéž určen především pro provádění javovského bytekódu, lze však provozovat i aplikace napsané pomocí jiného programovacího jazyka. Mikroprocesor je založen na jádru picoJava (viz předchozí kapitolu) a obsahuje ALU pro výpočty nad celými čísly, ALU pro výpočty nad hodnotami uloženými v systému plovoucí řádové tečky, tři čítače a časovače, řadič přerušení, šestnáctipinový universální port GPIO a v neposlední řadě i řadič paměti a sběrnice přímo na čipu – toto řešení podporuje paměti typu EDO DRAM, SDRAM, SRAM, ROM a Flash. Řadič sběrnice odpovídá normě PCI 2.1 a pracuje na nejnižší povolené frekvenci 33 MHz. Kromě toho je pomocí I/O řadiče podporována lokální sběrnice, která může mít bitovou šířku 8, 16 nebo 32 bitů.

Po hardwarové stránce je tento mikroprocesor postavený na 0,25mikronové technologii CMOS. Pracovní frekvence dosahuje u tohoto základního modelu 200 MHz, napájecí napětí jádra je na dnešní dobu poměrně vysoké: 2,5 V, spotřeba je však velmi nízká – pouze 4 watty. Pro napájení lokální sběrnice a PCI sběrnice je vyžadováno ještě přídavné napájecí napětí 3,3 V. Procesor obsahuje pouzdro typu PBGA (Ball Grid Array) s 316 piny. Další modely této série jsou však baleny do odlišných pouzder tak, aby odpovídaly požadavkům aplikací.

bitcoin_skoleni

6. Obsah dalšího pokračování

V závěrečné části tohoto seriálu si shrneme přednosti a zápory programovacího jazyka Forth, ukážeme si možnosti použití Forthu v současnosti a uvedeme si i odkazy na zajímavé internetovské stránky o tomto tématu.

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.