Obsah
1. Pohled pod kapotu JVM – je grafický subsystém Javy vhodný pro 2D hry?
2. Práce s bitmapami a některé příčiny nízkého grafického výkonu aplikací psaných v Javě
3. Zápis pixelů do bitmap typu BufferedImage: tři úrovně abstrakce
4. Demonstrační benchmark – vytvoření rastrového obrázku s gradientním přechodem
5. Nejvyšší úroveň abstrakce při zápisu pixelů: metoda BufferedImage.setRGB()
6. Střední úroveň abstrakce při zápisu pixelů: metoda WritableRaster.setPixel()
8. Nižší úroveň abstrakce: objekt DataBuffer a metoda DataBuffer.setElem()
9. Výsledky běhu demonstračního benchmarku
10. Úplný zdrojový kód demonstračního benchmarku
11. Repositář se zdrojovými soubory demonstračního benchmarku
1. Pohled pod kapotu JVM – je grafický subsystém Javy vhodný pro 2D hry?
V dnešní části seriálu o programovacím jazyce Java i o virtuálním stroji tohoto jazyka se budeme zabývat některými problémy, které musí vývojáři řešit při programování 2D her, popř. různých aplikací vyžadujících použití interaktivní 2D grafiky a animací. Platforma Javy totiž dává programátorům k dispozici poměrně velké množství balíčků a knihoven pro práci s 2D grafikou, což však může ve výsledku mít (poněkud paradoxně) negativní dopad na výkon vytvářených aplikací v případě, že vývojář zvolí nesprávný postup popř. například zbytečně použije vysokoúrovňové grafické operace nebo vytvoří takové formáty bitmap, při jejichž vykreslování nebude možné využít operací nabízených grafickými akcelerátory. Ukazuje se však, že minimálně v případě JDK6 a JDK7 lze při použití správných postupů v Javě dosáhnout takové rychlosti vykreslování, která je srovnatelná například s knihovnou SDL popř. s 2D funkcemi dostupnými v DirectX (poznámka: existuje i možnost volání funkcí SDL přímo z javovských programů, to však není zcela jednoduché a taktéž dosažená rychlost vykreslování není kvůli použití rozhraní JNI ideální).
V následujícím textu (zejména pak v navazující části seriálu) se zaměříme především na ty grafické operace, které lze najít v typických 2D hrách. Bude se tedy jednat o vytváření a vykreslování bitmap, využití paměti grafické karty či grafického akcelerátoru pro ukládání bitmap, nastavení grafického režimu (s exkluzivním přístupem ke framebufferu) atd. Problematikou tvorby grafického uživatelského rozhraní či vysokoúrovňovými grafickými operacemi se zabývat (alespoň prozatím) nebudeme, protože jak použití GUI (či vůbec vykreslování do okna) tak i operace nabízené rozhraními Graphics a Graphics2D většinou nejsou pro tvorbu interaktivních 2D her vhodné, i když i zde může JVM v některých případech využít možnosti grafických akcelerátorů (paradoxně jsou mnohdy tyto vysokoúrovňové operace použity v appletech, jejichž slabý grafický výkon pak mnohdy vede k odsouzení celé platformy Javy jakožto technologie zcela nevhodné pro tvorbu her).
2. Práce s bitmapami a některé příčiny nízkého grafického výkonu aplikací psaných v Javě
Typické 2D hry jsou založeny na neustálém vykreslování bitmap. Bitmapy jsou například použity ve funkci spritů, tj. rastrových obrázků s průhlednými či průsvitnými pixely. Sprity jsou použity pro reprezentaci objektů ve hře, mnohdy se však používají i pro tisk znaků atd. Bitmapy jsou ale i cílem veškerých vykreslovacích operací, protože i vlastní framebuffer (k němuž můžeme mít přímo z Javy přístup) je reprezentován bitmapou. Současné verze JDK dokážou při vykreslování bitmap využít operace nabízené grafickými akcelerátory (jedná se o operaci typu BitBLT/blit) a dokonce mohou programátorovi nabídnout přístup do framebufferu, prohazování předního a zadního bufferu (front buffer, back buffer) atd. – ovšem jen v tom případě, že programátor dodrží některá pravidla. Především je nutné všechny bitmapy, s nimiž se pracuje, vytvořit ve formátu kompatibilním s formátem framebufferu – to ostatně není žádná uměle zavedená podmínka, ale setkáme se s ní i v již zmíněné knihovně SDL.
Dále je nutné zabránit tomu, aby se měnil obsah bitmap změnou jednotlivých pixelů nebo přes rozhraní Graphics/Graphics2D. Pokud by totiž k těmto operacím došlo, ztratila by se jedna z největších výhod nabízených grafickými kartami a akcelerátory: možnost mít bitmapu uloženou přímo v obrazové paměti, takže se při operacích typu BitBLT/blit nebudou muset data přenášet po sběrnici z hlavní paměti do grafické karty, ale namísto toho se využije obecně velmi rychlá (a široká) interní sběrnice na akcelerátoru. Sice se to může na první pohled zdát trošku podivné, ale i tuto podmínku je možné v naprosté většině 2D her relativně snadno splnit.
Poznámka: význam výrazu BitBLT je následující:
Tato zkratka byla poprvé použita při programování systému počítače Xerox Alto, který používal pro zobrazování všech informací na monitoru výhradně rastrovou grafiku, konkrétně se jednalo o černobílé bitmapové obrázky. Při programování grafických rutin pro tento počítač a začleňování vytvářených rutin do operačního systému si autoři programového vybavení uvědomili, že poměrně velkou část již implementovaných funkcí lze zobecnit do jediné operace, která tyto funkce může nahradit. Těmito autory byli Daniel Ingalls, Larry Tesler, Bob Sproull a Diana Merry, kteří svoji zobecněnou rastrovou operaci pojmenovali BitBLT, neboli Bit Block Transfer. První část názvu, tj. slovo Bit naznačuje, že se jedná o operaci prováděnou nad bitmapami. Druhá polovina názvu, tj. zkratka BLT, byla odvozena ze jména instrukce pro blokový přenos dat, jenž byla používaná v assembleru počítače DEC PDP-10.
Obrázek 1: Grafické uživatelské rozhraní Smalltalku na počítači Xerox Alto.
Pomocí operace BitBLT lze provádět, jak její název naznačuje, blokové přenosy bitmap nebo jejich výřezů, popř. v rámci přenosu nad bitmapami provádět různé operace. První implementace operace BitBLT byla použita v roce 1975 ve Smalltalku-72 a od té doby ji najdeme prakticky v každé implementaci tohoto programovacího jazyka, která obsahuje i knihovny pro práci s grafikou (mj. se jedná i o Squeak). Pro Smalltalk-74 vytvořil Daniel Ingalls optimalizovanou variantu operace BitBLT implementovanou v mikrokódu. Operace BitBLT se tak stala součástí operačního systému a bylo ji možné volat jak z assembleru, tak i z programů napsaných v jazyce BCPL a samozřejmě i ze Smalltalku. Posléze se díky své univerzalitě tato funkce rozšířila i do mnoha dalších operačních systémů a grafických knihoven (v SDL ji najdeme ve funkci SDL_BlitSurface(), v Javě zase v metodě Graphics.drawImage()).
Obrázek 2: Část původního kódu operace BitBLT naprogramované Danielem Ingallsem.
3. Zápis pixelů do bitmap typu BufferedImage: tři úrovně abstrakce
V některých aplikacích je nutné přečíst či zapsat hodnotu jednotlivých pixelů do vytvořené bitmapy (pro jednoduchost nyní uvažujme bitmapy typu BufferedImage). Tuto zdánlivě jednoduchou a přímočarou operaci lze provést minimálně na třech úrovních abstrakce, v závislosti na tom, přes jaký typ objektu se bude k bitmapě a pixelům přistupovat. Na úrovni nejvyšší se pro čtení a zápis pixelů využívají operace BufferedImage.getRGB() a BufferedImage.setRGB(). Tyto operace jsou sice snadno pochopitelné i jednoduše použitelné, ovšem (což asi čtenáře tohoto článku příliš nepřekvapí) se jedná o ty nejpomalejší metody, které lze pro práci na úrovni jednotlivých pixelů použít.
Na nižší úrovni lze k pixelům uloženým v bitmapě přistupovat přes metody Raster.getPixel(), Raster.getPixels(), WritableRaster.setPixel() a WritableRaster.setPixels(). Samotný objekt typu Raster/WritableRaster lze získat snadno, jelikož je samotná bitmapa (BufferedImage) složena právě z instance toho typu objektu, který je doplněn o instanci objektu typu ColorModel. Jak rychlé jsou tyto operace si ukážeme na benchmarku.
Ovšem je dokonce možné použít metodu Raster.getDataBuffer() pro získání objektu typu DataBuffer. Zde se již nacházíme na nejnižší úrovni, protože se zde pracuje s jednotlivými pixely, u nichž musíme znát formát jejich uložení (bajt, short, …). Ve třídě Raster můžeme najít metody Raster.getElem() a Raster.setElem(). Zda se skutečně jedná o nejrychlejší možný způsob přístupu k jednotlivým pixelům nám opět prozradí benchmark.
4. Demonstrační benchmark – vytvoření rastrového obrázku s gradientním přechodem
Pro praktické otestování rychlosti několika různých způsobů zápisu barev pixelů byl vytvořen následující (poměrně jednoúčelový) benchmark. V tomto benchmarku je nejprve vytvořena bitmapa, přesněji řečeno objekt typu BufferedImage o rozlišení 512×512 pixelů. Pro větší přehlednost celého benchmarku byl zvolen jeden z nejjednodušších formátů bitmapy – každý pixel je zde reprezentován jediným bajtem, jehož hodnota vyjadřuje světlost pixelu. Jedná se o formát BufferedImage.TYPE_BYTE_GRAY. Benchmark následně spustí v pěti implementacích rozhraní PatternPainter metodu fillImageByPattern(). Každá třída implementující toto rozhraní by měla do předané bitmapy vykreslit gradientní přechod od černého pixelu (levý horní roh) k bílým pixelům. Vytvořená bitmapa by měla vypadat následovně:
Obrázek 3: Bitmapa vytvořená třídami implementujícími rozhraní PatternPainter.
Ve zdrojovém kódu benchmarku nejsou (prozatím) uvedeny jednotlivé implementace rozhraní PatternPainter, ty budou popsány v navazujících kapitolách:
import java.awt.Color; import java.awt.color.ColorSpace; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.WritableRaster; import java.io.File; import java.io.IOException; import javax.imageio.ImageIO; /** * Rozhrani implementovane vsemi tridami, ktere dokazou vykreslit * do bitmapy gradientni prechod. */ interface PatternPainter { public void fillImageByPattern(BufferedImage image); } /** * Test rychlosti ruznych zpusobu zapisu barev pixelu do rastroveho * obrazku. * * @author Pavel Tisnovsky */ public class ImageRendererTest { /** * Implementace jednotlivych trid, ktere zapisuji pixely do rastroveho obrazku. */ private static final PatternPainter[] patternPainters = new PatternPainter[5]; static { patternPainters[0] = new PatternPainterVersion1(); patternPainters[1] = new PatternPainterVersion2(); patternPainters[2] = new PatternPainterVersion3(); patternPainters[3] = new PatternPainterVersion4(); patternPainters[4] = new PatternPainterVersion5(); } /** * Prefix jmena souboru s vygenerovanou bitmapou. */ private static final String OUTPUT_FILE_NAME_PREFIX = "test"; /** * Horizontalni rozmer bitmapy. */ private static final int IMAGE_HEIGHT = 512; /** * Vertikalni rozmer bitmapy. */ private static final int IMAGE_WIDTH = 512; /** * Pocet iteraci zahrivaci faze benchmarku. */ private static final int WARMUP_ITERS = 10; /** * Pocet iteraci merene faze benchmarku. */ private static final int BENCHMARK_ITERS = 2; /** * Vytvoreni nove bitmapy se stupni sedi. * * @return nove vytvorena bitmapa */ private static BufferedImage createEmptyImage() { return new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_BYTE_GRAY); } /** * Zapis bitmapy na disk ve formatu PNG. * * @param image * testovaci bitmapa * @throws IOException */ private static void writeImageIntoFile(BufferedImage image, String fileName) throws IOException { ImageIO.write(image, "png", new File(fileName)); } /** * Zahrivaci faze benchmarku. */ private static void warmup() throws IOException { System.out.print("warmup begin "); for (int i = 0; i < WARMUP_ITERS; i++) { System.out.print(" " + i); for (PatternPainter painter : patternPainters) { warmup(painter); } } System.out.println(); } /** * Zahrivaci faze benchmarku volana pro kazdy PatternPainter. */ private static void warmup(PatternPainter patternPainter) { // vytvoreni bitmapy BufferedImage image = createEmptyImage(); // vyplneni bitmapy vzorkem patternPainter.fillImageByPattern(image); } /** * Vlastni benchmark. */ private static void benchmark() throws IOException { for (int i = 0; i < BENCHMARK_ITERS; i++) { System.out.println("benchmark #" + i + " begin"); int j = 0; for (PatternPainter painter : patternPainters) { benchmark(painter, ++j); } } } /** * Benchmark volany pro kazdy PatternPainter. */ private static void benchmark(PatternPainter patternPainter, int testNumber) throws IOException { // vytvoreni bitmapy BufferedImage image = createEmptyImage(); // cas zacatku vypoctu long t1 = System.nanoTime(); // vyplneni bitmapy vzorkem patternPainter.fillImageByPattern(image); // cas konce vypoctu long t2 = System.nanoTime(); System.out.println("Method #" + testNumber + ": " + (t2-t1) + " ns"); // zapis bitmapy na disk pro pozdejsi kontrolu writeImageIntoFile(image, OUTPUT_FILE_NAME_PREFIX + testNumber + ".png"); } /** * Spusteni benchmarku. * * @param args * @throws IOException */ public static void main(String[] args) throws IOException { warmup(); benchmark(); } }
5. Nejvyšší úroveň abstrakce při zápisu pixelů: metoda BufferedImage.setRGB()
Na nejvyšší úrovni abstrakce lze pro zápis barev jednotlivých pixelů použít metodu BufferedImage.setRGB(). Tato metoda je sice velmi jednoduchá na použití, ovšem je taktéž nejpomalejší, a to zejména z toho důvodu, že se většinou provádí i převod mezi barvovými prostory, jelikož metoda BufferedImage.setRGB() očekává, že barvy pixelů budou reprezentovány vektorem v prostoru sRGB. To mj. znamená, že při použití lineárního přechodu 0..255 dostaneme kvůli tomuto převodu mnohem tmavší obrázek, než je očekáváno. Následující kód je sice rychlejší, ovšem nepřesný:
// provest vypocet barev pixelu a jejich vykresleni for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int gray = (x+y) & 0xff; int rgb = (gray << 16 ) | (gray << 8) | (gray) | 0xff000000; image.setRGB(x, y, rgb); } }
Přesnější výpočet je velmi pomalý:
// provest vypocet barev pixelu a jejich vykresleni for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { float gray = ((x+y) & 0xff)/255.0f; int rgb = new Color(colorSpace, new float[] {gray, gray, gray}, 1.0f).getRGB(); image.setRGB(x, y, rgb); } }
Právě tento pomalejší výpočet je implementován ve třídě PatternPainterVersion1 použité v benchmarku:
/** * Varianta vyuzivajici metodu BufferedImage.setRGB(). * Poznamka: zde se navic uplatni gamma konverze. */ class PatternPainterVersion1 implements PatternPainter { /* (non-Javadoc) * @see PatternPainter#fillImageByPattern(java.awt.image.BufferedImage) */ public void fillImageByPattern(BufferedImage image) { // rozmery bitmapy final int width = image.getWidth(); final int height = image.getHeight(); ColorSpace colorSpace = ColorSpace.getInstance(ColorSpace.CS_GRAY); // provest vypocet barev pixelu a jejich vykresleni for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { float gray = ((x+y) & 0xff)/255.0f; int rgb = new Color(colorSpace, new float[] {gray, gray, gray}, 1.0f).getRGB(); image.setRGB(x, y, rgb); } } } }
6. Střední úroveň abstrakce při zápisu pixelů: metoda WritableRaster.setPixel()
Na střední úrovni abstrakce je pro zápis barev pixelů použita metoda WritableRaster.setPixel(). Poněkud obtížné je volání této metody, protože se vyžaduje předání pole s barvovými složkami pixelu. V našem případě bitmapy ve stupních šedi se předává jednoprvkové pole. Naivní implementace, v níž se pole vytváří uvnitř smyčky, může vypadat následovně:
/** * Varianta vyuzivajici metodu WritableRaster.setPixel(). */ class PatternPainterVersion2 implements PatternPainter { /* (non-Javadoc) * @see PatternPainter#fillImageByPattern(java.awt.image.BufferedImage) */ public void fillImageByPattern(BufferedImage image) { // rozmery bitmapy final int width = image.getWidth(); final int height = image.getHeight(); WritableRaster raster = image.getRaster(); // provest vypocet barev pixelu a jejich vykresleni for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { raster.setPixel(x, y, new int[] {x+y}); } } } }
7. Vylepšení předchozí metody
Předchozí implementaci lze snadno vylepšit, a to tak, že se pole vytvoří pouze jedenkrát, ovšem využito bude pro zápis každého pixelu. Namísto 512×512=262144 alokací jednoprvkového pole se tak provede alokace jediná, což je zaručeně rychlejší:
/** * Varianta vyuzivajici metodu WritableRaster.setPixel(), * pomocne pole se vytvari vne smycky. */ class PatternPainterVersion3 implements PatternPainter { /* (non-Javadoc) * @see PatternPainter#fillImageByPattern(java.awt.image.BufferedImage) */ public void fillImageByPattern(BufferedImage image) { // rozmery bitmapy final int width = image.getWidth(); final int height = image.getHeight(); WritableRaster raster = image.getRaster(); // pole pouzite uvnitr smycky int[] array = new int[1]; // provest vypocet barev pixelu a jejich vykresleni for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { array[0] = x+y; raster.setPixel(x, y, array); } } } }
Další vylepšení (jedná se skutečně o vylepšení? – viz výsledky) spočívá v tom, že se barvy všech pixelů zapíšou jedinou operací, a to pomocí metody WritableRaster.setPixels(). Nejprve je nutné připravit jednodimenzionální pole obsahující hodnoty všech pixelů, následně toto pole naplnit a přepsat je do bitmapy:
/** * Varianta vyuzivajici metodu WritableRaster.setPixels(). */ class PatternPainterVersion4 implements PatternPainter { /* (non-Javadoc) * @see PatternPainter#fillImageByPattern(java.awt.image.BufferedImage) */ public void fillImageByPattern(BufferedImage image) { // rozmery bitmapy final int width = image.getWidth(); final int height = image.getHeight(); WritableRaster raster = image.getRaster(); int[] array = new int[width * height]; // provest vypocet barev pixelu int i = 0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { array[i] = x + y; i++; } } // provest vykresleni vsech pixelu raster.setPixels(0, 0, width, height, array); } }
8. Nižší úroveň abstrakce: objekt DataBuffer a metoda DataBuffer.setElem()
Poslední možností je využití metody Raster.getDataBuffer() pro získání objektu typu DataBuffer. Zde se již nacházíme na nejnižší úrovni, kde lze pro zápis barvy pixelu použít metodu Raster.setElem():
/** * Varianta vyuzivajici metodu DataBuffer.setElem(). */ class PatternPainterVersion5 implements PatternPainter { /* (non-Javadoc) * @see PatternPainter#fillImageByPattern(java.awt.image.BufferedImage) */ public void fillImageByPattern(BufferedImage image) { // rozmery bitmapy final int width = image.getWidth(); final int height = image.getHeight(); // ziskani objektu obsahujiciho hodnoty vsech pixelu bitmapy DataBuffer dataBuffer = image.getRaster().getDataBuffer(); // provest vypocet barev pixelu a jejich vykresleni int i=0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { dataBuffer.setElem(i, x+y); i++; } } } }
9. Výsledky běhu demonstračního benchmarku
Podívejme se nyní na výsledky běhu benchmarku získané jak pro interpret, tak i pro JIT překladače typu klient a server. Výsledky si následně graficky porovnáme:
Režim interpretru (-Xint):
warmup begin 0 1 2 3 4 5 6 7 8 9 benchmark #0 begin Method #1: 17204373742 ns Method #2: 428374556 ns Method #3: 347463154 ns Method #4: 54061062 ns Method #5: 92748380 ns benchmark #1 begin Method #1: 17252366434 ns Method #2: 425173870 ns Method #3: 348450710 ns Method #4: 53940936 ns Method #5: 92857052 ns
Režim JIT klient (-client):
warmup begin 0 1 2 3 4 5 6 7 8 9 benchmark #0 begin Method #1: 8090542232 ns Method #2: 38028856 ns Method #3: 24550884 ns Method #4: 6392992 ns Method #5: 8662832 ns benchmark #1 begin Method #1: 8133280628 ns Method #2: 36035306 ns Method #3: 24821030 ns Method #4: 6696940 ns Method #5: 8680154 ns
Režim JIT server (-server):
warmup begin 0 1 2 3 4 5 6 7 8 9 benchmark #0 begin Method #1: 7744233134 ns Method #2: 29595102 ns Method #3: 21738516 ns Method #4: 3614426 ns Method #5: 922184 ns benchmark #1 begin Method #1: 7840402342 ns Method #2: 27052042 ns Method #3: 19576510 ns Method #4: 5522768 ns Method #5: 919950 ns
Režim okamžité kompilace (-Xcomp):
warmup begin 0 1 2 3 4 5 6 7 8 9 benchmark #0 begin Method #1: 8231745934 ns Method #2: 53280516 ns Method #3: 22943976 ns Method #4: 7597056 ns Method #5: 929728 ns benchmark #1 begin Method #1: 8384165306 ns Method #2: 29740932 ns Method #3: 22824410 ns Method #4: 3548496 ns Method #5: 928330 ns
Pokud budou v jednou grafu vyneseny i časy běhu operace BufferedImage.setRGB(), jasně vidíme, že se jedná o zdaleka nejpomalejší možnou operaci zápisu barev pixelů, a to zcela nezávisle na tom, zda je použit interpret či JIT překladač:
Zajímavější budou ostatní výsledky, pokud časy běhu operace BufferedImage.setRGB() z grafu odstraníme (mění nám totiž měřítko). Zde již jsou výsledky zajímavější a ukazují, že v režimu interpretru je nejvýhodnější nejdříve naplnit pole barvami pixelů a posléze použít metodu Raster.setPixels(). Na druhou stranu můžeme předpokládat, že se čistý interpret nebude nikdy a nikde používat, takže se soustřeďme na druhý, třetí a čtvrtou skupinu sloupců. Z výsledků vyplývá, že nejrychlejším způsobem zápisu barev pixelů bude použití nejnižší úrovně abstrakce, konkrétně metody DataBuffer.setElem(). Výjimkou je režim JIT klienta, kde je opět naplnění pole výhodnější, než neustále volání metody DataBuffer.setElem() (což ovšem dává smysl, když si uvědomíme, jak JIT klient NEoptimalizuje volání metod či rozbalení smyčky).
10. Úplný zdrojový kód demonstračního benchmarku
Pro přehlednost si ještě uvedeme kompletní zdrojový kód demonstračního benchmarku, včetně všech pěti implementací rozhraní PatternPainter:
import java.awt.Color; import java.awt.color.ColorSpace; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.WritableRaster; import java.io.File; import java.io.IOException; import javax.imageio.ImageIO; /** * Rozhrani implementovane vsemi tridami, ktere dokazou vykreslit * do bitmapy gradientni prechod. */ interface PatternPainter { public void fillImageByPattern(BufferedImage image); } /** * Varianta vyuzivajici metodu BufferedImage.setRGB(). * Poznamka: zde se navic uplatni gamma konverze. */ class PatternPainterVersion1 implements PatternPainter { /* (non-Javadoc) * @see PatternPainter#fillImageByPattern(java.awt.image.BufferedImage) */ public void fillImageByPattern(BufferedImage image) { // rozmery bitmapy final int width = image.getWidth(); final int height = image.getHeight(); ColorSpace colorSpace = ColorSpace.getInstance(ColorSpace.CS_GRAY); // provest vypocet barev pixelu a jejich vykresleni for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { float gray = ((x+y) & 0xff)/255.0f; int rgb = new Color(colorSpace, new float[] {gray, gray, gray}, 1.0f).getRGB(); image.setRGB(x, y, rgb); } } } } /** * Varianta vyuzivajici metodu WritableRaster.setPixel(). */ class PatternPainterVersion2 implements PatternPainter { /* (non-Javadoc) * @see PatternPainter#fillImageByPattern(java.awt.image.BufferedImage) */ public void fillImageByPattern(BufferedImage image) { // rozmery bitmapy final int width = image.getWidth(); final int height = image.getHeight(); WritableRaster raster = image.getRaster(); // provest vypocet barev pixelu a jejich vykresleni for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { raster.setPixel(x, y, new int[] {x+y}); } } } } /** * Varianta vyuzivajici metodu WritableRaster.setPixel(), * pomocne pole se vytvari vne smycky. */ class PatternPainterVersion3 implements PatternPainter { /* (non-Javadoc) * @see PatternPainter#fillImageByPattern(java.awt.image.BufferedImage) */ public void fillImageByPattern(BufferedImage image) { // rozmery bitmapy final int width = image.getWidth(); final int height = image.getHeight(); WritableRaster raster = image.getRaster(); // pole pouzite uvnitr smycky int[] array = new int[1]; // provest vypocet barev pixelu a jejich vykresleni for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { array[0] = x+y; raster.setPixel(x, y, array); } } } } /** * Varianta vyuzivajici metodu WritableRaster.setPixels(). */ class PatternPainterVersion4 implements PatternPainter { /* (non-Javadoc) * @see PatternPainter#fillImageByPattern(java.awt.image.BufferedImage) */ public void fillImageByPattern(BufferedImage image) { // rozmery bitmapy final int width = image.getWidth(); final int height = image.getHeight(); WritableRaster raster = image.getRaster(); int[] array = new int[width * height]; // provest vypocet barev pixelu int i = 0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { array[i] = x + y; i++; } } // provest vykresleni vsech pixelu raster.setPixels(0, 0, width, height, array); } } /** * Varianta vyuzivajici metodu DataBuffer.setElem(). */ class PatternPainterVersion5 implements PatternPainter { /* (non-Javadoc) * @see PatternPainter#fillImageByPattern(java.awt.image.BufferedImage) */ public void fillImageByPattern(BufferedImage image) { // rozmery bitmapy final int width = image.getWidth(); final int height = image.getHeight(); // ziskani objektu obsahujiciho hodnoty vsech pixelu bitmapy DataBuffer dataBuffer = image.getRaster().getDataBuffer(); // provest vypocet barev pixelu a jejich vykresleni int i=0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { dataBuffer.setElem(i, x+y); i++; } } } } /** * Test rychlosti ruznych zpusobu zapisu barev pixelu do rastroveho * obrazku. * * @author Pavel Tisnovsky */ public class ImageRendererTest { /** * Implementace jednotlivych trid, ktere zapisuji pixely do rastroveho obrazku. */ private static final PatternPainter[] patternPainters = new PatternPainter[5]; static { patternPainters[0] = new PatternPainterVersion1(); patternPainters[1] = new PatternPainterVersion2(); patternPainters[2] = new PatternPainterVersion3(); patternPainters[3] = new PatternPainterVersion4(); patternPainters[4] = new PatternPainterVersion5(); } /** * Prefix jmena souboru s vygenerovanou bitmapou. */ private static final String OUTPUT_FILE_NAME_PREFIX = "test"; /** * Horizontalni rozmer bitmapy. */ private static final int IMAGE_HEIGHT = 512; /** * Vertikalni rozmer bitmapy. */ private static final int IMAGE_WIDTH = 512; /** * Pocet iteraci zahrivaci faze benchmarku. */ private static final int WARMUP_ITERS = 10; /** * Pocet iteraci merene faze benchmarku. */ private static final int BENCHMARK_ITERS = 2; /** * Vytvoreni nove bitmapy se stupni sedi. * * @return nove vytvorena bitmapa */ private static BufferedImage createEmptyImage() { return new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_BYTE_GRAY); } /** * Zapis bitmapy na disk ve formatu PNG. * * @param image * testovaci bitmapa * @throws IOException */ private static void writeImageIntoFile(BufferedImage image, String fileName) throws IOException { ImageIO.write(image, "png", new File(fileName)); } /** * Zahrivaci faze benchmarku. */ private static void warmup() throws IOException { System.out.print("warmup begin "); for (int i = 0; i < WARMUP_ITERS; i++) { System.out.print(" " + i); for (PatternPainter painter : patternPainters) { warmup(painter); } } System.out.println(); } /** * Zahrivaci faze benchmarku volana pro kazdy PatternPainter. */ private static void warmup(PatternPainter patternPainter) { // vytvoreni bitmapy BufferedImage image = createEmptyImage(); // vyplneni bitmapy vzorkem patternPainter.fillImageByPattern(image); } /** * Vlastni benchmark. */ private static void benchmark() throws IOException { for (int i = 0; i < BENCHMARK_ITERS; i++) { System.out.println("benchmark #" + i + " begin"); int j = 0; for (PatternPainter painter : patternPainters) { benchmark(painter, ++j); } } } /** * Benchmark volany pro kazdy PatternPainter. */ private static void benchmark(PatternPainter patternPainter, int testNumber) throws IOException { // vytvoreni bitmapy BufferedImage image = createEmptyImage(); // cas zacatku vypoctu long t1 = System.nanoTime(); // vyplneni bitmapy vzorkem patternPainter.fillImageByPattern(image); // cas konce vypoctu long t2 = System.nanoTime(); System.out.println("Method #" + testNumber + ": " + (t2-t1) + " ns"); // zapis bitmapy na disk pro pozdejsi kontrolu writeImageIntoFile(image, OUTPUT_FILE_NAME_PREFIX + testNumber + ".png"); } /** * Spusteni benchmarku. * * @param args * @throws IOException */ public static void main(String[] args) throws IOException { warmup(); benchmark(); } }
11. Repositář se zdrojovými soubory demonstračního benchmarku
Následuje – v tomto seriálu již tradiční – kapitola s odkazy na zdrojové kódy uložené do Mercurial repositáře. V následující tabulce najdete odkazy na prozatím nejnovější verzi dnes popsaného demonstračního příkladu (benchmarku):
# | Zdrojový soubor/skript | Umístění souboru v repositáři |
---|---|---|
1 | ImageRendererTest.java | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/d43fb5c88eb4/jvm/gfx/ImageRendererTest/ImageRendererTest.java |
2 | Test.sh | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/d43fb5c88eb4/jvm/gfx/ImageRendererTest/Test.sh |
3 | Test.bat | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/d43fb5c88eb4/jvm/gfx/ImageRendererTest/Test.bat |
12. Odkazy na Internetu
- Java quick guide: JVM Instruction Set (tabulka všech instrukcí JVM)
http://www.mobilefish.com/tutorials/java/java_quickguide_jvm_instruction_set.html - The JVM Instruction Set
http://mpdeboer.home.xs4all.nl/scriptie/node14.html - MultiMedia eXtensions
http://softpixel.com/~cwright/programming/simd/mmx.phpi - SSE (Streaming SIMD Extentions)
http://www.songho.ca/misc/sse/sse.html - Timothy A. Chagnon: SSE and SSE2
http://www.cs.drexel.edu/~tc365/mpi-wht/sse.pdf - Intel corporation: Extending the Worldr's Most Popular Processor Architecture
http://download.intel.com/technology/architecture/new-instructions-paper.pdf - SIMD architectures:
http://arstechnica.com/old/content/2000/03/simd.ars/ - GC safe-point (or safepoint) and safe-region
http://xiao-feng.blogspot.cz/2008/01/gc-safe-point-and-safe-region.html - Safepoints in HotSpot JVM
http://blog.ragozin.info/2012/10/safepoints-in-hotspot-jvm.html - Java theory and practice: Synchronization optimizations in Mustang
http://www.ibm.com/developerworks/java/library/j-jtp10185/ - How to build hsdis
http://hg.openjdk.java.net/jdk7/hotspot/hotspot/file/tip/src/share/tools/hsdis/README - Java SE 6 Performance White Paper
http://www.oracle.com/technetwork/java/6-performance-137236.html - Lukas Stadler's Blog
http://classparser.blogspot.cz/2010/03/hsdis-i386dll.html - How to build hsdis-amd64.dll and hsdis-i386.dll on Windows
http://dropzone.nfshost.com/hsdis.htm - PrintAssembly
https://wikis.oracle.com/display/HotSpotInternals/PrintAssembly - The Java Virtual Machine Specification: 3.14. Synchronization
http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-3.html#jvms-3.14 - The Java Virtual Machine Specification: 8.3.1.4. volatile Fields
http://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.3.1.4 - The Java Virtual Machine Specification: 17.4. Memory Model
http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4 - The Java Virtual Machine Specification: 17.7. Non-atomic Treatment of double and long
http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.7 - Open Source ByteCode Libraries in Java
http://java-source.net/open-source/bytecode-libraries - ASM Home page
http://asm.ow2.org/ - Seznam nástrojů využívajících projekt ASM
http://asm.ow2.org/users.html - ObjectWeb ASM (Wikipedia)
http://en.wikipedia.org/wiki/ObjectWeb_ASM - Java Bytecode BCEL vs ASM
http://james.onegoodcookie.com/2005/10/26/java-bytecode-bcel-vs-asm/ - BCEL Home page
http://commons.apache.org/bcel/ - Byte Code Engineering Library (před verzí 5.0)
http://bcel.sourceforge.net/ - Byte Code Engineering Library (verze >= 5.0)
http://commons.apache.org/proper/commons-bcel/ - BCEL Manual
http://commons.apache.org/bcel/manual.html - Byte Code Engineering Library (Wikipedia)
http://en.wikipedia.org/wiki/BCEL - BCEL Tutorial
http://www.smfsupport.com/support/java/bcel-tutorial!/ - Bytecode Engineering
http://book.chinaunix.net/special/ebook/Core_Java2_Volume2AF/0131118269/ch13lev1sec6.html - Bytecode Outline plugin for Eclipse (screenshoty + info)
http://asm.ow2.org/eclipse/index.html - Javassist
http://www.jboss.org/javassist/ - Byteman
http://www.jboss.org/byteman - Java programming dynamics, Part 7: Bytecode engineering with BCEL
http://www.ibm.com/developerworks/java/library/j-dyn0414/ - The JavaTM Virtual Machine Specification, Second Edition
http://java.sun.com/docs/books/jvms/second_edition/html/VMSpecTOC.doc.html - The class File Format
http://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html - javap – The Java Class File Disassembler
http://docs.oracle.com/javase/1.4.2/docs/tooldocs/windows/javap.html - javap-java-1.6.0-openjdk(1) – Linux man page
http://linux.die.net/man/1/javap-java-1.6.0-openjdk - Using javap
http://www.idevelopment.info/data/Programming/java/miscellaneous_java/Using_javap.html - Examine class files with the javap command
http://www.techrepublic.com/article/examine-class-files-with-the-javap-command/5815354 - aspectj (Eclipse)
http://www.eclipse.org/aspectj/ - Aspect-oriented programming (Wikipedia)
http://en.wikipedia.org/wiki/Aspect_oriented_programming - AspectJ (Wikipedia)
http://en.wikipedia.org/wiki/AspectJ - EMMA: a free Java code coverage tool
http://emma.sourceforge.net/ - Cobertura
http://cobertura.sourceforge.net/ - jclasslib bytecode viewer
http://www.ej-technologies.com/products/jclasslib/overview.html