Search code examples
javalistmathjava-streamvectorization

How to use Java Streams API to do element wise multiplication between 2 lists of same dimension


I have 2 lists like:

List<Double> margins =  Arrays.asList(1.0,2.0,3.0,4.0,5.0);
List<Integer> quantity = Arrays.asList(1,2,3,4,5);

I want to do element wise multiplication between them, I know I can do it with standard for loop, but I was wondering if we can achieve the same via the Stream API by making the process faster and less resource heavy?

Something like what Python ML does with NumPy arrays; instead of making a for loop they vectorize the thing which makes it faster.


Solution

  • Something like what Python ML does with numpy arrays; instead of making a for loop they vectorize the thing which makes it faster.

    If you're interested in Vectorization, then Stream API is not what you're looking for.

    Since Java 16 we have Vector API as an incubating feature (meaning that it's not the final state of the API, it's there for testing and collecting feedback, and you shouldn't use it in production).

    In order to use incubating features in Java, you need to do some extra work to get files from an incubating module imported.

    One of the ways to make sure that classes from the package jdk.incubator.vector would be imported is to create a module to explicitly specify that it requires this package.

    Consider a simple test project having the following folder-structure:

    - [src]
      - [main]
        - [java]
          - module-info.java
          - [vectorization.test]
            - Main.java
          - resources
    
    // other things
    

    module-info.java - here we're requesting Vector API's files:

    module vectorization.test {
        requires jdk.incubator.vector;
    }
    

    Main.java

    package vectorization.test;
    
    import jdk.incubator.vector.DoubleVector;
    import jdk.incubator.vector.VectorSpecies;
    
    import java.util.Arrays;
    
    public class Main {
        
        public static void main(String[] args) {
            double[] margins = {1.0, 2.0, 3.0, 4.0, 5.0};
            double[] quantity = {1, 2, 3, 4, 5};
            double[] result = new double[margins.length];
        
            multiply(margins, quantity, result);
        
            System.out.println(Arrays.toString(result));
        }
        
        public static final VectorSpecies<Double> SPECIES = DoubleVector.SPECIES_PREFERRED;
        
        public static void multiply(double[] margins, double[] quantity, double[] result) {
            int i = 0;
            for (; i < SPECIES.loopBound(margins.length); i += SPECIES.length()) {
                DoubleVector marginsVector = DoubleVector.fromArray(SPECIES, margins, i);
                DoubleVector quantityVector = DoubleVector.fromArray(SPECIES, quantity, i);
                DoubleVector resultVector = marginsVector.mul(quantityVector);
                resultVector.intoArray(result, i);
            }
            for (; i < margins.length; i++) {
                result[i] = margins[i] * quantity[i];
            }
        }
    }
    

    Output:

    [1.0, 4.0, 9.0, 16.0, 25.0]
    

    For more information related to the Vector API, have a look at the JEP 426.

    Speaking of Streams they are not more performant than plain loop (if case of sequential streams), in fact they are slower because streams require creation of additional objects to perform iteration, to make transformation, and to accumulate the result. No magic here.

    Parallel streams might be faster, but it's a tool which should be used with care because you also might get the opposite effect. You need your task to be parallelizable, there should free CPU cores for your threads to do the job, and amount of data should be massive to make the usage of parallel streams justifiable (and still have to measure the performance to find out whether it makes difference in your case).

    One of the main goals of introducing Functional programming features in Java was to provide a concise and well readable way of structuring the code.

    For instance, that how you can populate your resulting array of products by using Arrays.setAll():

    Arrays.setAll(result, i -> margins[i] * quantity[i]);