Hlavní navigace

Pohled pod kapotu JVM – je grafický subsystém Javy vhodný pro 2D hry?

26. 11. 2013
Doba čtení: 21 minut

Sdílet

Dnešní část seriálu o jazyce Java pojednává o problematice grafického subsystému Javy, zejména s ohledem na tvorbu 2D her, popř. interaktivních programů či dem. Ukazuje se, že při volbě správných technik lze i v této oblasti dosáhnout uspokojivých výsledků srovnatelných s jinými technologiemi (SDL, Pygame atd.).

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()

7. Vylepšení předchozí metody

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

12. Odkazy na Internetu

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 GraphicsGraphics2D 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()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()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()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).

ict ve školství 24

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):

12. Odkazy na Internetu

  1. Java quick guide: JVM Instruction Set (tabulka všech instrukcí JVM)
    http://www.mobilefish.com/tu­torials/java/java_quickgu­ide_jvm_instruction_set.html
  2. The JVM Instruction Set
    http://mpdeboer.home.xs4a­ll.nl/scriptie/node14.html
  3. MultiMedia eXtensions
    http://softpixel.com/~cwrig­ht/programming/simd/mmx.phpi
  4. SSE (Streaming SIMD Extentions)
    http://www.songho.ca/misc/sse/sse­.html
  5. Timothy A. Chagnon: SSE and SSE2
    http://www.cs.drexel.edu/~tc365/mpi-wht/sse.pdf
  6. Intel corporation: Extending the Worldr's Most Popular Processor Architecture
    http://download.intel.com/techno­logy/architecture/new-instructions-paper.pdf
  7. SIMD architectures:
    http://arstechnica.com/ol­d/content/2000/03/simd.ar­s/
  8. GC safe-point (or safepoint) and safe-region
    http://xiao-feng.blogspot.cz/2008/01/gc-safe-point-and-safe-region.html
  9. Safepoints in HotSpot JVM
    http://blog.ragozin.info/2012/10/sa­fepoints-in-hotspot-jvm.html
  10. Java theory and practice: Synchronization optimizations in Mustang
    http://www.ibm.com/develo­perworks/java/library/j-jtp10185/
  11. How to build hsdis
    http://hg.openjdk.java.net/jdk7/hot­spot/hotspot/file/tip/src/sha­re/tools/hsdis/README
  12. Java SE 6 Performance White Paper
    http://www.oracle.com/technet­work/java/6-performance-137236.html
  13. Lukas Stadler's Blog
    http://classparser.blogspot­.cz/2010/03/hsdis-i386dll.html
  14. How to build hsdis-amd64.dll and hsdis-i386.dll on Windows
    http://dropzone.nfshost.com/hsdis.htm
  15. PrintAssembly
    https://wikis.oracle.com/dis­play/HotSpotInternals/Prin­tAssembly
  16. The Java Virtual Machine Specification: 3.14. Synchronization
    http://docs.oracle.com/ja­vase/specs/jvms/se7/html/jvms-3.html#jvms-3.14
  17. The Java Virtual Machine Specification: 8.3.1.4. volatile Fields
    http://docs.oracle.com/ja­vase/specs/jls/se7/html/jls-8.html#jls-8.3.1.4
  18. The Java Virtual Machine Specification: 17.4. Memory Model
    http://docs.oracle.com/ja­vase/specs/jls/se7/html/jls-17.html#jls-17.4
  19. The Java Virtual Machine Specification: 17.7. Non-atomic Treatment of double and long
    http://docs.oracle.com/ja­vase/specs/jls/se7/html/jls-17.html#jls-17.7
  20. Open Source ByteCode Libraries in Java
    http://java-source.net/open-source/bytecode-libraries
  21. ASM Home page
    http://asm.ow2.org/
  22. Seznam nástrojů využívajících projekt ASM
    http://asm.ow2.org/users.html
  23. ObjectWeb ASM (Wikipedia)
    http://en.wikipedia.org/wi­ki/ObjectWeb_ASM
  24. Java Bytecode BCEL vs ASM
    http://james.onegoodcooki­e.com/2005/10/26/java-bytecode-bcel-vs-asm/
  25. BCEL Home page
    http://commons.apache.org/bcel/
  26. Byte Code Engineering Library (před verzí 5.0)
    http://bcel.sourceforge.net/
  27. Byte Code Engineering Library (verze >= 5.0)
    http://commons.apache.org/pro­per/commons-bcel/
  28. BCEL Manual
    http://commons.apache.org/bcel/ma­nual.html
  29. Byte Code Engineering Library (Wikipedia)
    http://en.wikipedia.org/wiki/BCEL
  30. BCEL Tutorial
    http://www.smfsupport.com/sup­port/java/bcel-tutorial!/
  31. Bytecode Engineering
    http://book.chinaunix.net/spe­cial/ebook/Core_Java2_Volu­me2AF/0131118269/ch13lev1sec6­.html
  32. Bytecode Outline plugin for Eclipse (screenshoty + info)
    http://asm.ow2.org/eclipse/index.html
  33. Javassist
    http://www.jboss.org/javassist/
  34. Byteman
    http://www.jboss.org/byteman
  35. Java programming dynamics, Part 7: Bytecode engineering with BCEL
    http://www.ibm.com/develo­perworks/java/library/j-dyn0414/
  36. The JavaTM Virtual Machine Specification, Second Edition
    http://java.sun.com/docs/bo­oks/jvms/second_edition/html/VMSpec­TOC.doc.html
  37. The class File Format
    http://java.sun.com/docs/bo­oks/jvms/second_edition/html/Clas­sFile.doc.html
  38. javap – The Java Class File Disassembler
    http://docs.oracle.com/ja­vase/1.4.2/docs/tooldocs/win­dows/javap.html
  39. javap-java-1.6.0-openjdk(1) – Linux man page
    http://linux.die.net/man/1/javap-java-1.6.0-openjdk
  40. Using javap
    http://www.idevelopment.in­fo/data/Programming/java/mis­cellaneous_java/Using_javap­.html
  41. Examine class files with the javap command
    http://www.techrepublic.com/ar­ticle/examine-class-files-with-the-javap-command/5815354
  42. aspectj (Eclipse)
    http://www.eclipse.org/aspectj/
  43. Aspect-oriented programming (Wikipedia)
    http://en.wikipedia.org/wi­ki/Aspect_oriented_program­ming
  44. AspectJ (Wikipedia)
    http://en.wikipedia.org/wiki/AspectJ
  45. EMMA: a free Java code coverage tool
    http://emma.sourceforge.net/
  46. Cobertura
    http://cobertura.sourceforge.net/
  47. jclasslib bytecode viewer
    http://www.ej-technologies.com/products/jclas­slib/overview.html

Autor článku

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