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