13

I am trying to calculate the sum of squares of values in the list. Below are three variations which all calculates the required value. I want to know which one is the most efficient. I am expecting the third one to be more efficient as auto boxing is only done once.

    // sum of squares
    int sum = list.stream().map(x -> x * x).reduce((x, y) -> x + y).get();
    System.out.println("sum of squares: " + sum);

    sum = list.stream().mapToInt(x -> x * x).sum();
    System.out.println("sum of squares: " + sum);

    sum = list.stream().mapToInt(x -> x).map(x -> x * x).sum();
    System.out.println("sum of squares: " + sum);
Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
Rob Wilkinson
  • 1,131
  • 5
  • 18
  • 34
  • 3
    While in this particular case, the double-map adds little, there are cases where separating a transform into multiple parts will make the computation more readable or simpler. (And, as the mapping operation increase in weight, the overhead of the extra stage becomes proportionally less.) – Brian Goetz Sep 09 '15 at 00:14

3 Answers3

14

When in doubt, test! Using jmh, I get the following results on a list of 100k elements (in microseconds, smaller is better):

Benchmark                        Mode  Samples     Score    Error  Units
c.a.p.SO32462798.for_loop        avgt       10   119.110    0.921  us/op
c.a.p.SO32462798.mapToInt        avgt       10   129.702    1.040  us/op
c.a.p.SO32462798.mapToInt_map    avgt       10   129.753    1.516  us/op
c.a.p.SO32462798.map_reduce      avgt       10  1262.802   12.197  us/op
c.a.p.SO32462798.summingInt      avgt       10   134.821    1.203  us/op

So you have, from faster to slower:

  • for(int i : list) sum += i*i;
  • mapToInt(x -> x * x).sum() and mapToInt(x -> x).map(x -> x * x).sum()
  • collect(Collectors.summingInt(x -> x * x))
  • map(x -> x * x).reduce((x, y) -> x + y).get()

Note that the results are very much dependent on the JIT optimisations. If the logic in the mapping is more complex, some of the optimisations may be unavailable (longer code = less inlining) in which case the streams versions may take 4-5x more time than the for loop - but if that logic is CPU heavy the difference will reduce again. Profiling your actual application will give you more information.


Benchmark code for reference:

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
public class SO32462798 {

  List<Integer> list;

  @Setup public void setup() {
    list = new Random().ints(100_000).boxed().collect(toList());
  }

  @Benchmark public int for_loop() {
    int sum = 0;
    for (int i : list) sum += i * i;
    return sum;
  }

  @Benchmark public int summingInt() {
    return list.stream().collect(Collectors.summingInt(x -> x * x));
  }

  @Benchmark public int mapToInt() {
    return list.stream().mapToInt(x -> x * x).sum();
  }

  @Benchmark public int mapToInt_map() {
    return list.stream().mapToInt(x -> x).map(x -> x * x).sum();
  }

  @Benchmark public int map_reduce() {
    return list.stream().map(x -> x * x).reduce((x, y) -> x + y).get();
  }
}
assylias
  • 321,522
  • 82
  • 660
  • 783
  • 2
    It would be nice to add also a plain-old-loop implementation as the reference. Like `int sum = 0;for(int i : list) sum+=i*i; return sum;`. One more alternative is to use `list.stream().collect(Collectors.summingInt(x -> x*x));` – Tagir Valeev Sep 09 '15 at 05:13
  • 1
    @TagirValeev `summingInt` is faster than mapToInt + sum - interesting. For loop is in its own category as expected. – assylias Sep 09 '15 at 09:41
  • 2
    Btw, using `list = new Random().ints(100_000).boxed().collect(Collectors.toList());` in `setup()` would be neat! – Tagir Valeev Sep 09 '15 at 09:46
  • Thanks @assylias for your detailed answer with example. From the above bench mark I see that the applications which use streams can be less performing than the ones which use for loops. May be we need compiler optimization to convert to for loops. Please comment. – Rob Wilkinson Sep 09 '15 at 10:08
  • 1
    @JohnLiva I'm a bit surprised by the difference (I would expect less overhead for the streams). I have posted a follow-up question [here](http://stackoverflow.com/questions/32476864/first-warmup-much-faster-than-average). – assylias Sep 09 '15 at 10:18
  • 1
    @JohnLiva Allowing for a longer warmup period produced much better results for the streams which then perform almost in line with the for loop. I doubt that these results can be obtained in a real application with more complex logic in the mapping though. – assylias Sep 09 '15 at 10:51
1

I expect the second one to be the fastest.

There is boxing in neither the second nor the third example (if the list contains already boxed elements). But, there is unboxing.

Your second example might have two unboxing (one for every x in x*x), while the third does unboxing only once. However, unboxing is fast and I think it does not worth to optimize that as a longer pipeline with an additional function call will certainly slow it down.

Sidenote: in general, you should not expect Streams to be faster than regular iterations on arrays or lists. When doing mathematical calculations where speed matters (like this) it's better to go the other way: simply iterate through the elements. If your output is an aggregated value, then aggregate it, if it is a mapping, then allocate a new array or list of the same size and fill it with the calculated values.

Tamas Hegedus
  • 28,755
  • 12
  • 63
  • 97
  • 2
    Since the question’s operation is calculating a sum, there is no point in creating an array. Or in other words, in this case there is nothing a manual iteration could do better than the stream operation… – Holger Sep 08 '15 at 17:12
  • @Holger You're right about the mapping. However you can still save `list.size()` function calls with regular iteration. – Tamas Hegedus Sep 08 '15 at 17:27
  • Which function calls are you talking about? When you stream over, e.g. an `ArrayList`, there is no `size()` function call at all. – Holger Sep 09 '15 at 08:26
  • @Holger I'm talking about calling the mapping function (like `x -> x*x`) `list.size()` times. Sry for not making this clear. – Tamas Hegedus Sep 09 '15 at 09:17
  • If you use a stream, the list’s spliterator will call a function method *n*-1 times for a list of size *n*. If you use an ordinary foreach loop, your code will invoke `hasNext()` *n* +1 times and `next()` *n* times on the iterator. There’s no free lunch… – Holger Sep 09 '15 at 09:34
  • Well, I can't argue with that, that's definitely true. It should be benchmarked once :) – Tamas Hegedus Sep 09 '15 at 09:37
  • Thanks @hege_hegedus for your quick response – Rob Wilkinson Sep 09 '15 at 10:09
-1

The mapToInt() method, a variation of the map operation (variations like mapToInt(), mapToDouble(), and so on create type-specialized streams such as IntStream and DoubleStream). Whenever we need to use any IntStream Class' method after mapping the stream we can use mapToINT().

Mehrose
  • 69
  • 2
  • 9