Making ImageIO.read() 15% faster for JPEGs
Philip Race
philip.race at oracle.com
Fri Feb 13 20:58:08 UTC 2026
So you'd end up with a BI that is of TYPE_CUSTOM ?
That might make decoding a bit faster but rendering would be very slow.
I'm not sure I'd want that trade-off.
-phil.
On 1/6/26 8:59 AM, Jeremy Wood wrote:
> 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();
> }
> }
> }
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/client-libs-dev/attachments/20260213/dc7abc42/attachment-0001.htm>
More information about the client-libs-dev
mailing list