Hlavní navigace

Nechte Go plavat, teď sviští Java

Jaroslav Tulach

Go se používá hlavně v systémech, kde je potřeba rychlý start, malé nároky na paměť, snadná komunikace mezi více vlákny a kde se použití Céčka jeví příliš nebezpečné. Ale Go není jediná cesta, jak něco takového dosáhnout.

Doba čtení: 7 minut

Přednášku a původní článek v angličtině Forget Go! Go, Java, go! jsem si připravil již v létě. Nyní se ale na Rootu objevila série o jazyce Go, a tak jsem zmobilizoval síly a přeložil svůj původní text do češtiny. Snad bude tento alternativní pohled na jazyk Go užitečný.

Go je programovací jazyk, který pochází z kuchyně Googlu. Když byl v roce 2009 poprvé představen veřejnosti, tak se hlavně zmiňovalo, že:

Po těch téměř deseti letech od svého uvedení již vyprchalo prvotní nadšení, ale jazyk Go si stále drží dost ze své popularity. Především je podporován a používán velkou firmou. Díky tomu funguje. To co dělá, dělá dobře a zřejmě to tak bude ještě hodně dlouho. Navíc se objevily nové projekty, které na Go staví: Docker je napsán v Go a protože je to nejpopulárnější kontajner, podporuje svým úspěchem i popularitu Go.

Nechte Go plavat!

Go se používá hlavně v systémech, kde je potřeba rychlý start, malé nároky na paměť, snadná komunikace mezi více vlákny a kde se použití klasického Céčka jeví příliš nebezpečné. Go skvěle splňuje funkci systémového jazyka, ale navíc, díky automatické správě paměti, eliminuje již od základu chyby, které se v jiných systémových jazycích dají tak snadno napáchat. Pokud potřebujete jazyk s podobnými vlastnostmi, tak se můžete domnívat, že Go je ta pravá volba. Může být, ale určitě to není jediné možné řešení. Pojďme prozkoumat jednu možnou alternativu: Zkusme použít Javu!

Co? Na co? Na Javu!?

Cože? Javu? Toho pomalého, interpretovaného bumbrlíčka, který zabere všechnu dostupnou paměť, aby ukojil nároky toho svého nenažraného virtualního stroje? Toho stroje, který se chová téměř jak samostatný operační systém? Tu Javu, kterou každý správný systémák nenávidí? No tak tu zrovna ne. Trochu jinou Javu, ale nejprve se pojďme podívat na vlastnosti Javy jako jazyka:

Zní to povědomě? Ano, jazyk Java nabízí ty samé výhody, které jsou připisovány jazyku Go. Když k tomu připočítáme dvacet let soutěžení o nejlepší vývojové prostředí, knihovnu či framework, tak dostaneme výsledek: refaktorování, automatické nápovědy při psaní kódu, podpora rozličných knihoven. To vše vytváří systém, který je vpravdě robustní.

Efektivní spouštění s native-image

Klasický interpret Javy je společně s přidruženým JIT překladačem zatížen nutností udržovat výrazné množství meta dat. JIT dokáže generovat vysoce optimalizovaný kód. Někdy optimalizovaný až příliš. Pokud se pak objeví situace, se kterou se při překladu nepočítalo, tak je nutné přepnout zpět do interpretru. Aby toto bylo možné, tak se při běhu udržují megabajty a megabajty meta dat. To není zrovna vhodné, pokud člověk cílí na malá zařízení, tak jako Go. Naštěstí však existuje řešení: Pojďme si představit část GraalVM nazvanou native-image.

V principu je native-image AOT překladač Javy (či jiných jazyků běžících nad JVM) se snadno použitelnou interoperabilitou s Céčkem a jinými knihovnami operačního systému. native-image dokáže dát programu napsanému v Javě chování, které má Go. To kromě jiného znamená:

  • okamžitý start procesu
  • žádná zbytečná zátěž metadaty z interpretru a dynamického překladu
  • nízké paměťové požadavky

Zahoďte předsudky, že je Java nutně nenažraná. Teď můžeme zkombinovat to nejlepší z Javy (či jiného JVM jazyka jako je Kotlin či Scala) a ahead-of-time překladem native-image, což nám vytvoří ekosystém, který řeší stejné problémy jako Go a dělá to překvapivě dobře!

Co je to vlastně ta rychlost?

Porovnávat rychlost různých jazyků není triviální úkol a dá se při něm velmi dobře podvádět. Na druhou stranu se dá najít sada základních operací, kterou každý Turingově úplný jazyk musí podporovat (if větvení, while cykly, přístup do paměti, alokaci na haldě či její čištění). Porovnáním těchto operací pak dle mého názoru můžeme změřit Turingovu rychlost jazyků relativně přesně. Právě o to se pokouším ve svém projektu, který měří rychlost různých jazyků na variantě již antickým Řekům známého algoritmu na výpočet prvočísel.

Zkusme si porovnat Go, Céčko a native-image Javu. Následující výsledky byly získány z https://github.com/jtulach/sieve verze a671eb115 přeložené pomocí go1.9.2 na Ubuntu 16.04:

sieve/go$ ./go | grep Hundred
Hundred thousand prime numbers in 253 ms
Hundred thousand prime numbers in 263 ms
Hundred thousand prime numbers in 261 ms
Hundred thousand prime numbers in 270 ms
Hundred thousand prime numbers in 250 ms
Hundred thousand prime numbers in 277 ms
Hundred thousand prime numbers in 241 ms

nyní změňme adresář a zkusme ten samý algoritmus napsaný v Céčku:

sieve/c$ sieve | grep Hundred
Hundred thousand prime numbers in 100 ms
Hundred thousand prime numbers in 101 ms
Hundred thousand prime numbers in 98 ms
Hundred thousand prime numbers in 102 ms
Hundred thousand prime numbers in 101 ms
Hundred thousand prime numbers in 108 ms
Hundred thousand prime numbers in 97 ms

A hle: Go je pomalejší než Céčko, což může být daní za bezpečnost jazyka. Prostě mít automatickou správu paměti zřejmě není zcela zadarmo. Teď zkusme ten samý program spustit pomocí native-image:

sieve$ mvn -f java/algorithm/ -Dnative.image=/graalvm/bin/native-image package
sieve$ java/algorithm/target/sieve | grep Hundred
Hundred thousand primes computed in 184 ms
Hundred thousand primes computed in 143 ms
Hundred thousand primes computed in 196 ms
Hundred thousand primes computed in 222 ms
Hundred thousand primes computed in 182 ms
Hundred thousand primes computed in 158 ms
Hundred thousand primes computed in 150 ms
Hundred thousand primes computed in 176 ms
Hundred thousand primes computed in 138 ms
Hundred thousand primes computed in 140 ms
Hundred thousand primes computed in 146 ms
Hundred thousand primes computed in 170 ms
Hundred thousand primes computed in 156 ms

Opět je tato verze o něco pomalejší než Céčko, ale kupodivu je rychlejší než Go. To je dobrá zpráva: zdá se, že opravdu není nutné Javu odepisovat. Pomocí native-image přeložená Java je rychlostně přinejmenším srovnatelná s Go.

Vše v jednom „exáči“

Jedna ze skvělých věcí, kterou jazyk Go nabízí, je možnost přeložit vše do jednoho spustitelného souboru. Důsledkem je pak rychlejší start, neboť není třeba nahrávat žádné dynamické knihovny, a snazší přenositelnost: stačí zkopírovat jen jeden soubor a program může běžet na novém stroji. V případě mého Eratosthénova síta je velikost tohoto souboru menší než dva megabyty:

sieve$ ls -lh go
1,9M go
1,8K sieve.go

Překlad pomocí native-image funguje podobně jako překladač jazyka Go. Takže i native-image nám vygeneruje jeden spustitelný soubor:

sieve$ mvn -f java/algorithm/ -Dnative.image=/graalvm/bin/native-image package
sieve$ ls -lh java/algorithm/target/sieve
4,8M java/algorithm/target/sieve

Výsledný soubor je o trošku větší (pravděpodobně důsledek snahy dodržovat striktně sémantiku javovského virtuálního stroje), ale i tato velikost zůstává řádově srovnatelná s exáčem vytvořeným Go. Samozřejmě zůstávají stejné výhody. Není nutno kopírovat JVM – stačí si vzít jen tento exáč, dát jej do Dockeru či jiného malého systému a použít jej.

Možnost volby

Go je jazyk navržený od začátku tak, aby řešil problémy při psaní nízkoúrovňových programů spjatých s operačním systémem. Go to zvládá velmi dobře. Na druhou stranu Go zvolilo přístup, který v sobě má jistá omezení. Go překladač je od základu napsán Go týmem. Nevyužívá žádné z obecně rozšířených systémů jako je GCC či LLVM. Vše je od začátku do konce napsáno Googlem. To se samozřejmě negativně odráží na šíři podporovaných fíčurek, ale i na rychlosti. Go tým prostě není nafukovací a nemůže všechny požadavky řešit tak rychle, jak by si uživatelé přáli.

Tímto omezením Java ani native-image utilitka netrpí. JVM je tu s námi přes dvacet let. Její interpretr i kompilátor jsou akceptovány a používány napříč celým programátorským světem a díky tomu byly neuvěřitelně zoptimalizovány. Celá GraalVM na této infrastruktuře staví. I native-image z toho těží: kupříkladu její AOT překladač je úplně stejný jako JIT překladač použitý v GraalVM pokud běží v klasickém HotSpot módu. Z toho plyne, že ty samé optimalizace, které se aplikují na váš program běžící v normálním JDKáčku jsou aplikovatelné i při generování exáče pomocí native-image.

V neposlední řadě se v případě psaní kódu v Javě pozitivně ukazují důsledky líté soutěže o nejlepší vývojové prostředí. Díky dvaceti letům soutěžení existuje spousta refaktoringů, kódovacích tipů, knihoven a nástrojů, které native-image umí okamžitě využít. Navíc native-image není omezena jen na jeden jazyk. Pište si ve Scale či Kotlinu či jakémkoliv jiném jazyce, který lze přeložit do bajtkódu.

Správná volba je jen na vás.

Teď sviští Java!

native-image poskytuje vcelku životaschopnou alternativu k Go. Nejen, že dokáže těžit z šíře Java ekosystému, ale občas dokáže vygenerovat i rychlejší kód. Při tom všem zachovává ty aspekty kvůli nimž Go vzniklo: native-image umí vygenerovat soběstačný spustitelný soubor, který nastartuje v mžiku a při tom má nízké nároky na operační paměť.

S native-image se z Javy a ostatních JVM jazyků stávají jazyky pro vývoj nízkoúrovňových programů pro operační systémy! Od teď je Java univerzální programovací jazyk.

Příště se mrkneme na rozhraní native-image pro přístup k Céčkovým knihovnám – tedy operačnímu systému.

Našli jste v článku chybu?