Making ImageIO.read() 15% faster for JPEGs
Jeremy Wood
mickleness at gmail.com
Tue Jan 6 16:59:50 UTC 2026
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/20260106/0a88cb17/attachment-0001.htm>
More information about the client-libs-dev
mailing list