Unleashing Parallelism: From Loops to SIMD – Speeding Up Image Grayscale Conversion with Java’s Vector API
This example compares two approaches for converting an image to grayscale: a traditional loop-based method and a vectorized approach using the Java Vector API.
Scenario:
We have a 2D array image
representing the image data, where each element is an array containing red, green, and blue (RGB) values for a pixel.
1. Traditional Loop Approach:
public static void convertToGrayscaleLoop(int[][] image) {
int width = image.length;
int height = image[0].length;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int red = image[y][x][0];
int green = image[y][x][1];
int blue = image[y][x][2];
int grayscale = (red + green + blue) / 3;
image[y][x][0] = grayscale;
image[y][x][1] = grayscale;
image[y][x][2] = grayscale;
}
}
}
This code iterates through each pixel in the image using nested loops. For each pixel, it calculates the average of the RGB values and updates all three channels (red, green, blue) with the grayscale value.
2. Vectorized Approach with Java Vector API:
import java.util.vector.DoubleVector;
public static void convertToGrayscaleVector(int[][] image) {
int width = image.length;
int height = image[0].length;
// Separate color channels into arrays
double[] red = new double[width * height];
double[] green = new double[width * height];
double[] blue = new double[width * height];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
red[y * width + x] = image[y][x][0];
green[y * width + x] = image[y][x][1];
blue[y * width + x] = image[y][x][2];
}
}
// Create DoubleVectors from color arrays
DoubleVector redVector = DoubleVector.of(red);
DoubleVector greenVector = DoubleVector.of(green);
DoubleVector blueVector = DoubleVector.of(blue);
// Calculate average using element-wise operations
DoubleVector grayscaleVector = redVector.add(greenVector).add(blueVector);
grayscaleVector = grayscaleVector.div(3.0);
// Convert grayscale vector back to array
double[] grayscaleArray = grayscaleVector.toArray();
// Update image with grayscale values
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
image[y][x][0] = (int) grayscaleArray[y * width + x];
image[y][x][1] = (int) grayscaleArray[y * width + x];
image[y][x][2] = (int) grayscaleArray[y * width + x];
}
}
}
This code takes a vectorized approach:
- It separates the RGB channels into individual double arrays.
- It creates
DoubleVector
instances from these arrays. - It performs element-wise addition and division using vectorized operations on
redVector
,greenVector
, andblueVector
. This leverages SIMD instructions for parallel processing. - Finally, it converts the result vector back to an array and updates the image with the grayscale values.
Benefits of Vectorized Approach:
- Parallelism: By operating on multiple elements within vectors simultaneously using SIMD instructions, the CPU can potentially process multiple pixels in a single instruction, leading to faster grayscale conversion compared to the loop-based approach.
- Efficiency: Vectorized operations often benefit from optimized memory access patterns and efficient use of CPU resources.