Bystroushaak's blog / Czech section / Jak se stát programátorem / Scriptovací versus kompilované jazyky

Scriptovací versus kompilované jazyky

Po krátkém uvážení jsem se rozhodl přidat i upravenou odpověď na jeden email, kde jsme se s tazatelem bavili o rozdílu mezi kompilovanými a interpretovanými programovacími jazyky.

Existují v zásadě čtyři možné abstraktní úrovně typů programovacích jazyků:

  1. Psaní přímo ve strojovém kódu.
  1. Psaní v assembleru.
  1. Psaní v kompilovaných jazycích.
  1. Psaní v interpretovaných jazycích.

Jednotlivé úrovně se liší podle složitosti, možností, které programátorovi dávají při manipulaci s počítačem a efektivitou práce.

1. Psaní ve strojovém kódu

Psaní ve strojovém kódu je nejtěžší, protože nepoužíváte programovací jazyk, ale píšete přímo instrukce procesoru, což jsou sekvence čísel.

Pokud chceš pochopit jak to vypadá, tady je malá ukázka z programu pro smazání souboru s názvem "soubor.txt". Celý program má 45724 řádek těchto čísel.

00b5a80 6878 5a61 3176 5f35 645f 6c67 7469 7265
00b5a90 6c61 3632 3532 464d 415a 6179 5f00 3344
00b5aa0 7473 3864 6164 6574 6974 656d 3431 6579
00b5ab0 7261 7349 654c 7061 6559 7261 4e46 4e61
00b5ac0 6962 625a 5f00 3344 7473 3564 7473 6964
00b5ad0 346f 6946 656c 4237 4379 7568 6b6e 6535
00b5ae0 706d 7974 784d 4e46 5a64 0062 445f 6334
00b5af0 726f 3665 6874 6572 6461 3431 6946 6562
00b5b00 4572 6378 7065 6974 6e6f 5f37 435f 616c
00b5b10 7373 005a 445f 7333 6474 6334 6e6f 3176
00b5b20 5f39 545f 7434 7865 5474 7941 5461 5477
00b5b30 7941 5a61 7434 7865 4674 7941 7761 7941
00b5b40 5a61 7941 0061 445f 7333 6474 6438 7461
00b5b50 7465 6d69 3965 6954 656d 664f 6144 3679
00b5b60 5f5f 7463 726f 464d 614e 6969 5a69 3353
00b5b70 7473 3864 6164 6574 6974 656d 5439 6d69
00b5b80 4f65 4466 7961 5f00 3344 7473 3564 6172
00b5b90 676e 3565 5f33 545f 7235 7465 6f72 4154

Jedná se o čísla v hexadecimálním formátu, což je často používaný způsob vyjádření obsahu buněk v paměti. Ten tlustější sloupec úplně nalevo je adresa instrukcí, které pak následují na zbytku řádku a jsou pro přehlednost odděleny mezerami po 16 bitech (2 bajtech).

Vezměme si první dva sloupečky z prvního řádku:

00b5a80 6878

Co to vlastně znamená? Nic jiného, než že paměťová buňka číslo 00b5a80 (744064 v decimální soustavě) má hodnotu 6878 hexadecimálně, což je číslo 26744 v normální, lidmi používané decimální soustavě.

Až tahle buňka poleze do procesoru, na drátech se postupně objeví jedničky a nuly (napětí a absence napětí, jako když blikáte žárovkou) 0110100001111000.

Co přesně znamená číslo 6878? Nemám tušení. Zkoušel jsem to hledat v tabulce opkódů pro x86, ale nic jsem nenašel. Pravděpodobně je to špatně zarovnané, možná v tom hraje i roli endianita.

Je to složité, co? Lidé si něco takového nejsou schopní zapamatovat, proto vznikl assembler.

2. Psaní v Assembleru

Viděli jste předchozí čísla - nedá se z nich moc poznat, o co se jedná. Proto vznikl assembler - jazyk, který vyjadřuje, co daná čísla znamenají. Dá se z něj jednodušeji (ne však jednoduše) pochopit, co program dělá, ale pořád je to dost nízkoúrovňové a proto ho většina programátorů nepoužívá, pokud vyloženě nemusí.

Čísla z předchozího programu dovede takzvaný disassembler (program pro převod existujících programů na zdrojový kód v assembleru) převést na zkratky instrukcí pro procesor.

8082e74 <_Dmain>:
 8082e74:55                   push   %ebp
 8082e75:8b ec                mov    %esp,%ebp
 8082e77:ff 35 b4 e2 0a 08    pushl  0x80ae2b4
 8082e7d:ff 35 b0 e2 0a 08    pushl  0x80ae2b0
 8082e83:e8 18 17 00 00       call   80845a0 <_D3std4file6removeFxAaZv>
 8082e88:31 c0                xor    %eax,%eax
 8082e8a:5d                   pop    %ebp
 8082e8b:c3                   ret
 8082e8c:90                   nop
 8082e8d:90                   nop
 8082e8e:90                   nop
 8082e8f:90                   nop

Bohužel jsem kdysi při psaní odpovědi vybral jinou část programu než u předchozího výpisu, což je možné poznat podle řady čísel vlevo, které opět označují adresy paměťových buněk. V minulém případě se jednalo o čísla okolo 00b5a80, nyní se díváme na adresu 8082e74, což je v programu o dost dál.

Za adresami instrukcí následuje sekvence čísel a znaků - například 55, nebo na druhém řádku 8bec. To jsou kódy instrukcí tak, jak je vidí procesor a jak byly zobrazeny ve výpisu nahoře.

Napravo je pak příkaz assembleru, například push %ebp, nebo call 80845a0 <_D3std4file6removeFxAaZv>.

Kódy vlevo jsou tam přidány, aby bylo jasně vidět, že každému číslu odpovídají patřičné instrukce (číslu c3, tedy 195 v decimální soustavě odpovídá instrukce ret).

Pokud by jsme program psali v assembleru a nepoužívali jen náhled na již existující kód z disassembleru, tak by celá ukázka vypadala nějak takto:

<_Dmain>:
push   %ebp
mov    %esp,%ebp
pushl  0x80ae2b4
pushl  0x80ae2b0
call   80845a0 <_D3std4file6removeFxAaZv>
xor    %eax,%eax
pop    %ebp
ret
nop
nop
nop
nop

Příkaz assembleru nám umožňuje zjistit (či specifikovat), co procesor udělá.


Paměťová buňka je nejmenší část, se kterou je možné v paměti pracovat. Podle architektury to může být 8, 16, 32 či 64 bitů, tedy jedniček a nul. Do buňky je možné zapisovat a číst z ní.

Představit si to můžete jako řadu formulářů, které určitě důvěrně znáte z internetových stránek, například přihlašování do emailů. Do každého políčka ve formuláři můžete buďto zapsat číslo, nebo z něj číslo přečíst.

V podstatě všechny instrukce assembleru pracují s těmito buňkami. Něco vezmou, dají to někam, provedou s tím něco a pak kamsi zapíšou výsledek. Z buňky do buňky, miliardkrát za vteřinu.



Podle toho co má instrukce za parametry se mění způsob, jakým svůj úkol provede. Například jimi lze určit, kterou buňku kam předá:

mov    %esp,%ebp

Tohle v podstatě říká, ať procesor přesune obsah buňky s názvem ebp do buňky esp. Kdybych to měl vyjádřit graficky, tak je to:

esp <- ebp

Assembler pořád není moc intuitivní. Kdybyste chtěli tímhle způsobem naprogramovat něco užitečného pro moderní počítače, musíte napsat desítky až stovky tisíc řádek podobných instrukcí. Není to dostatečně abstraktní.

Dejme tomu, že chcete třeba napsat program, který smaže soubor - pokud byste ho psali v assembleru, musíte prvně přesouvat čísla (či znaky) z buněk do buněk, dokud se v paměti nevytvoří souvislá řada znaků:

 _ _ _ _ _ _ _ _ _ _ _
|s|o|u|b|o|r|.|t|x|t|0|
 - - - - - - - - - - -

Jak už jsem psal, počítač pracuje s čísly, tedy ve skutečnosti by obsah buněk v paměti vypadal nějak takto:

 ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
|115|111|117| 98|111|114| 46|116|120|116|  0|
 --- --- --- --- --- --- --- --- --- --- ---

či v hexa:

 __ __ __ __ __ __ __ __ __ __ __
|73|6f|75|62|6f|72|2e|74|78|74|00|
 -- -- -- -- -- -- -- -- -- -- --

Poté stačí zavolat nějakou funkci jádra operačního systému, které někam (třeba do buňky eax) uloží adresu začátku této sekvence čísel. Funkce operačního systému si pak přečte buňky na které jste jí dali adresu (kde má skončit pozná podle nuly na konci), smazala soubor z disku a uložila návratový kód do nějaké standardní buňky (třeba zase eax). Podle tohoto kódu by jste pak zjistili, jestli došlo k chybě, nebo se podařilo soubor smazat v pořádku.

Když se někdo koukne na kód takového programu, bude mít stovky až tisíce řádek různých instrukcí a bude velice nepřehledný, takže se v něm za půl roku pravděpodobně nevyznáte a ni vy a to jste jeho tvůrci.


Pokud se někde uvádí, že procesor funguje na frekvenci třeba 3GHz, znamená to, že provede 3 000 000 000 taktů za vteřinu. Během jednoho taktu moderní procesory zpracují jednu, či více instrukcí, takže, když to pro názornost podstatně zjednoduším a vynechám některé důležité detaily, dá se zjednodušeně říct, že procesor vykoná za vteřinu 3 000 000 000 řádek assembleru.

Docela dobré, ne?


Další věc je, že assemblery jsou pro každý druh procesoru jiné. Jiný assembler má tvůj počítač, jiný má tvůj mobil a jiný má počítač v autě, nebo ten co řídí semafor. Někde se buňka eax jmenuje jinak, jinde vůbec neexistuje. Některé mají minimální šířku 8 bitů, jiné 64.

Proto se moc nevyplatí v něm programovat, protože pro každý procesor se musíš naučit nový assembler a programy, které jdou na procesoru typu x86_64 nepůjdou na x86, či mipsel.

3. Psaní v kompilovaných jazycích

Jak jste měli možnost vidět - assembler je sice o dost přehlednější než strojový kód, ale pořád je dost nízkoúrovňový a programování v něm jej docela náročné a složité.

Proto vznikly takzvané kompilované jazyky - jedná se o jazyky, kde programátor popíše v nějakém relativně příjemném jazyku co chce aby se stalo do textového souboru. Zdrojový kód může vypadat třeba takto:

import std.stdio;
import std.file : remove;

int main(string[] args){
   remove("soubor.txt");

   return 0;
}

Ten se pak předá programu zvanému kompilátor, který ho převede na strojový kód, tedy spustitelný soubor třeba s příponou .exe, takže například remove.exe.

Tento soubor se pak dá spouštět na počítačích se stejným typem procesoru a operačního systému. Když přijdete k počítači s jiným typem procesoru a operačního systému, jednoduše si vezmete znova zdrojový kód, opět ho zkompilujete a převedete na strojový kód daného procesoru.

Výše uvedená ukázka je v programovacím jazyce D. Je už docela přehledná - všimni si řádku s nápisem:

remove("soubor.txt");

To už je docela přehledné, ne? I začátečník v angličtině z toho pozná, o co jde.


Osm řádků v programovacím jazyce D se přeloží na 74985 řádků kódu v assembleru, což představuje 728KB strojového kódu.

To znamená, že kompilátor za mě vytvořil desítky tisíc řádků v assembleru. Prakticky by jich mohlo být o dost méně, ale použil jsem jazyk, který mi to jako programátorovi dost usnadňuje. Kdybych to psal v C, je výsledek obvykle menší .exe, ale zase bych toho musel obvykle napsat víc (což nutně neplatí pro tuhle ukázku).

4. Psaní v interpretovaných jazycích

Program v assembleru měl stovky až tisíce řádků. Zdrojový soubor v kompilovaném programovacím jazyce D měl osm řádků, program v interpretovaném jazyce python jde ještě dále a bude mít jen dva:

import os
os.remove("soubor.txt")

Jednoduché, ne? Prvním řádkem řeknete, že chcete pracovat s operačním systémem. Druhým řádkem operačnímu systému dáte příkaz, aby odstranil soubor s názvem soubor.txt.

Interpretované jazyky za tuto jednoduchost platí většími nároky na paměť a procesor.

Na rozdíl od kompilovaných programovacích jazyků nejsou překládány do strojového kódu, se kterým umí pracovat procesor a tedy z nich obvykle nedostanete .exe. Místo toho jsou předané programu zvanému interpreter, který je prochází řádek po řádku a když narazí na nějakou instrukci, zavolá podprogram, který provede to co mu instrukce určuje. Tento podprogram může mít třeba deset tisíc řádků v jazyce C a stovky tisíc řádků v jazyce assembler.

V praxi to znamená, že interpretované jazyky nikdy nejdou do procesoru, vždy jdou do interpreteru, který je (obvykle) napsaný v kompilovaném jazyce. Interpreter v podstatě posílá části sebe sama v závislosti na tom, na jakou část zdrojového kódu právě narazí.

Díky tomu, že nejdou do procesoru přímo fungují na všech procesorech kde pro ně byl jednou zkompilován interpreter.

Python je dost silný jazyk - jeden jeho řádek vydá za několik řádků v D, přibližně za 10 až 20 řádků v C a za několik set až tisíc v assembleru, proto jsem chtěl, aby jste s ním začínali a v původním článku jsem ho tolik doporučoval.

Shrnutí

Abych to shrnul, obecně platí následující vztahy:

Python spadá do kategorie 4 - je interpretovaný. To znamená, že je lehký na naučení, efektivní, ale některé věci, jako třeba operační systém v něm jen tak neuděláte. To se může zdát jako nevýhoda, ale jelikož to má být váš tvůj první programovací jazyk, tak to taková nevýhoda ve skutečnosti není.

Když navíc někdy narazíte na problém, že z pythonu potřebujete volat kód v úrovních 1-3, tak můžete. Python totiž umí volat knihovny napsané v jazycích nižší úrovně.

Doufám že jsem to vysvětlil dostatečně jasně, i když je mi jasné, že to většina začátečníků to asi nepochopí, rozhodně ne na první přečtení.

Become a Patron