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 redVectorgreenVector, and blueVector. 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.

You may also like...