Question

I was just playing around with Java 8 and comparing a few things with Java 6 using a program to calculate the sum of even numbers in a large list

Java 8

public class ListOperationsJava8 {

    static List<BigInteger> list = new LinkedList<>();

    public static void main(String[] args) {
        createList();

        long start = System.currentTimeMillis();

        /*System.out.println(list.parallelStream().
                filter(n -> n.mod(new BigInteger("2")).equals(BigInteger.ZERO)).
                        mapToInt(BigInteger::intValue).sum());  --> gives result        -1795017296 */

        System.out.println(list.parallelStream().
                filter(n -> n.mod(new BigInteger("2")).equals(BigInteger.ZERO)).
                    mapToLong(BigInteger::longValue).sum());

        long end = System.currentTimeMillis();

        System.out.println("Time taken using Java 8:  " + (end - start) + " ms");
    }

    private static void createList() {
        for (int i = 0; i < 100000; i++) {
            list.add(new BigInteger(String.valueOf(i)));
        }
    }
}

Java 6

public class ListOperationsClassic {

    static List<BigInteger> list = new LinkedList<BigInteger>();

    public static void main(String[] args) {
        createList();

        long start = System.currentTimeMillis();

        BigInteger sum = BigInteger.ZERO;

        for(BigInteger n : list) {
            if(n.mod(new BigInteger("2")).equals(BigInteger.ZERO))
                sum = sum.add(n);
        }

        System.out.println(sum);

        long end = System.currentTimeMillis();

        System.out.println("Time taken using Java 6: " + (end - start) + " ms");
    }

    private static void createList() {
        for (int i = 0; i < 100000; i++) {
            list.add(new BigInteger(String.valueOf(i)));
        }
    }
}

I have two questions

  1. In Java 8 code, initially I used mapToInt(BigInteger::intValue).sum()) to reduce the value , but got a negative result -1795017296! Why? The expected result 2499950000 itself is within the range that can be expressed by an int as I understand.
  2. I ran the code several times, I always found the code in Java 8 taking upto 5 times more time than the code using Java 6. What that could mean? For an operation of this scale, it is not worth using parallelStream and/or reduce operations and plain old for loop is better?

Here are one of the results:

2499950000
Time taken using Java 6: 52 ms

2499950000
Time taken using Java 8:  249 ms
Was it helpful?

Solution

Here is my quick and dirty benchmark, allowing for JIT warm-up and GC-ing betwen each test. The results:

  • for loop: 686 ms
  • lamdbda: 681 ms
  • parallel lambda: 405 ms

Note that I have modified your code to make the three tests as equivalent as possible - in particular, for lambdas, I'm using a reduction to add the BigIntegers instead of converting to long.
I have also removed the unnecessary creation of new BigInteger("2") at each iteration.

public class Test1 {

    static List<BigInteger> list = new LinkedList<>();
    static BigInteger TWO = new BigInteger("2");

    public static void main(String[] args) {
        createList();

        long sum = 0;

        //warm-up
        for (int i = 0; i < 100; i++) {
            sum += forLoop().longValue();
            sum += lambda().longValue();
            sum += parallelLambda().longValue();
        }

        {
            System.gc();
            long start = System.currentTimeMillis();
            for (int i = 0; i < 100; i++) sum += forLoop().longValue();
            long end = System.currentTimeMillis();
            System.out.println("Time taken using for loop:  " + (end - start) + " ms");
        }

        {
            System.gc();
            long start = System.currentTimeMillis();
            for (int i = 0; i < 100; i++) sum += lambda().longValue();
            long end = System.currentTimeMillis();
            System.out.println("Time taken using lambda:  " + (end - start) + " ms");
        }

        {
            System.gc();
            long start = System.currentTimeMillis();
            for (int i = 0; i < 100; i++) sum += parallelLambda().longValue();
            long end = System.currentTimeMillis();
            System.out.println("Time taken using parallelLambda:  " + (end - start) + " ms");
        }
    }

    private static void createList() {
        for (int i = 0; i < 100000; i++) {
            list.add(new BigInteger(String.valueOf(i)));
        }
    }

    private static BigInteger forLoop() {
        BigInteger sum = BigInteger.ZERO;

        for(BigInteger n : list) {
            if(n.mod(TWO).equals(BigInteger.ZERO))
                sum = sum.add(n);
        }
        return sum;
    }

    private static BigInteger lambda() {
        return list.stream().
                filter(n -> n.mod(TWO).equals(ZERO)).
                reduce(ZERO, BigInteger::add);
    }

    private static BigInteger parallelLambda() {
        return list.parallelStream().
                filter(n -> n.mod(TWO).equals(ZERO)).
                reduce(ZERO, BigInteger::add);
    }
}

OTHER TIPS

For your first question: Your value may have been greater than Integer.MAX_VALUE, and thus overflowing, which means it will be represented externally as starting at Integer.MIN_VALUE. Overflow is the keyword here.

For the second part, I do not know exactly why it differs, probably can be found in the bytecode ultimately, but is this really an issue, or a case of premature optimization? If it is the first, then you should worry, if it's the second case, then you should not pay attention to it being slower.

One difference I observe from your code though is that, with:

  • Java 6: You use one BigInteger sum, and call the .add() method on it.
  • Java 8: You use some long variable, and internally that variable gets added to.

Also, you are using a parallelStream() here for calculations that are very fast, this may lead to the issue that creating the new underlying threads actually costs more time than just using a normal linear stream().

Moreover, if you are really measuring the speed, then you should do more tests than a single run per testcase, the timing can be dependant on other factors too - The JVM, your CPU's clock speed, etc.

As last edit, your Java 8 code does actually not represent your Java 6 code, it should be:

final BigInteger sum = BigInteger.ZERO;
list.stream()
.filter(n -> n.mod(new BigInteger("2")).equals(BigInteger.ZERO))
.forEach(n -> { sum = sum.add(n); });

To fully represent your Java 6 code, be aware though that the introduction of sum is not really a good thing here, and this code will not work at all if you are using it for parallel computations.

One last edit, to correctly show how it should be done in Java 8, which looks correct, works for parallel versions and maybe even can pick up additional performance in linear versions:

    Optional<BigInteger> sum = list.stream()
            .filter(n -> n.mod(new BigInteger("2")).equals(BigInteger.ZERO))
            .reduce((n1, n2) -> n1.add(n2));
    System.out.println(sum.get());

Here I am using the reduce() operator, which takes a BinaryOperator<T> as argument, which I use to add the BigIntegers together.

One caveat is that it comes with the fact that the stream may be empty, so it does not know whether there is a value, so it returns an Optional<T>.

Please carefully note that normally you need to call sum.isPresent() to check whether it actually has a value, but in this we know that there must be a value as !list.isEmpty(), and thus we proceed to call sum.get() directly.

Lastly, I have tested the different versions on my PC with 1 million numbers:

  • Java 6: Roughly 190 ~ 210ms.
  • Java 8 your code: Roughly 160 ~ 220ms.
  • Java 8, mine linear: Roughly 180 ~ 260ms.
  • Java 8, mine parallel: Roughly 180 ~ 270ms.

So, as this was not really proper microbenchmarking, I think the conclusion is that it does not really matter what you use for this kind of purposes.

You can simply call reduce to add directly in the bigdecimal rather than converting into long.

list.parallelStream()
.filter(n -> n.mod(new BigInteger("2")).equals(BigInteger.ZERO))
.reduce(BigDecimal.ZERO, BigDecimal::add)
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top