I’m exploring the performance of loading BufferedImages. I recently noticed that ImageIO.read() can be made ~15% faster by changing the BufferedImage type (so we'd avoid a RGB -> BGR conversion).

IMO this wouldn’t be too hard to program, but it might be considered too invasive to accept. Is there any interest/support in exploring this idea if I submit a PR for it?

Specifically this is the performance I’m observing on my MacBook:

Benchmark Mode Cnt Score Error Units JPEG_Default_vs_RGB.measureDefaultImageType avgt 15 42.589 ± 0.137 ms/op JPEG_Default_vs_RGB.measureRGBImageType avgt 15 35.624 ± 0.589 ms/op

The first “default” approach uses ImageIO.read(inputStream).

The second “RGB” approach creates a BufferedImage target with a custom ColorModel that is similar to TYPE_3BYTE_BGR, except it reverse the colors so they are ordered RGB.

This derives from the observation that in JPEGImageReader we create a one-line raster field that uses this 3-byte RGB model. Later in acceptPixels() we call target.setRect(x, y, raster) . Here target is the WritableRaster of the final BufferedImage. By default it will be a 3-byte BGR. So we’re spending 15+% of our time converting RGB-encoded data (from raster) to BGR-encoded data (for target).

So the “pros” of my proposal should include a faster loading time for many JPEG images. I’d argue ImageIO should always default to the fastest (reasonable) implementation possible.

IMO the major “con” is: target.getType() would change from BufferedImage.TYPE_3BYTE_BGR to BufferedImage.TYPE_CUSTOM . This doesn’t technically violate any documentation that I know of, but it seems (IMO) like something some clients will have made assumptions about, and therefore some downstream code may break. (And maybe other devs here can identify other problems I’m not anticipating.)

Any thoughts / feedback?

Regards,
 - Jeremy

Below is the JMH code used to generate the output above:

package org.sun.awt.image;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.Random;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 20)
@Fork(3)
@State(Scope.Thread)
public class JPEG_Default_vs_RGB {

    byte[] jpgImageData;

    @Setup
    public void setup() throws Exception {
        jpgImageData = createImageData(2_500);
    }

    @Benchmark
    public void measureDefaultImageType(Blackhole bh) throws Exception {
        BufferedImage bi = readJPG(false);
        bi.flush();
        bh.consume(bi);
    }

    @Benchmark
    public void measureRGBImageType(Blackhole bh) throws Exception {
        BufferedImage bi = readJPG(true);
        bi.flush();
        bh.consume(bi);
    }

private BufferedImage readJPG(boolean useRGBTarget) throws Exception {
        Iterator<ImageReader> readers;
try (ByteArrayInputStream byteIn = new ByteArrayInputStream(jpgImageData)) {
            if (!useRGBTarget)
                return ImageIO.read(byteIn);

readers = ImageIO.getImageReaders(ImageIO.createImageInputStream(byteIn));
            if (!readers.hasNext()) {
throw new IOException("No reader found for the given file.”);
            }
        }

        ImageReader reader = readers.next();
try (ByteArrayInputStream byteIn = new ByteArrayInputStream(jpgImageData)) {
            reader.setInput(ImageIO.createImageInputStream(byteIn));

            int width = reader.getWidth(0);
            int height = reader.getHeight(0);

// this is copied from how BufferedImage sets up a TYPE_3BYTE_BGR image,
            // except we use {0, 1, 2} to make it an RGB image:
            ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
            int[] nBits = {8, 8, 8};
            int[] bOffs = {0, 1, 2};
ColorModel colorModel = new ComponentColorModel(cs, nBits, false, false,
                    Transparency.OPAQUE,
                    DataBuffer.TYPE_BYTE);
WritableRaster raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE,
                    width, height, width * 3, 3, bOffs, null);

BufferedImage rgbImage = new BufferedImage(colorModel, raster, false, null);

            ImageReadParam param = reader.getDefaultReadParam();
            param.setDestination(rgbImage);

            reader.read(0, param);

            return rgbImage;
        } finally {
            reader.dispose();
        }
    }

    /**
     * Create a large sample image stored as a JPG
     *
     * @return the byte representation of the JPG image.
     */
private static byte[] createImageData(int squareSize) throws Exception {
        BufferedImage bi = new BufferedImage(squareSize, squareSize,
                BufferedImage.TYPE_INT_RGB);
        Random r = new Random(0);
        Graphics2D g = bi.createGraphics();
        for (int a = 0; a < 20000; a++) {
            g.setColor(new Color(r.nextInt(0xffffff)));
            int radius = 10 + r.nextInt(90);
g.fillOval(r.nextInt(bi.getWidth()), r.nextInt(bi.getHeight()),
                    radius, radius);
        }
        g.dispose();

        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            ImageIO.write(bi, "jpg", out);
            return out.toByteArray();
        }
    }
}

Reply via email to