Существует ли какое-либо преимущество вызова карты после mapToInt, где когда-либо требовалось

Я пытаюсь вычислить сумму квадратов значений в списке. Ниже приведены три варианта, которые все вычисляют требуемое значение. Я хочу знать, какой из них наиболее эффективен. Я ожидаю третьего чтобы быть более эффективным, поскольку автоматический бокс выполняется только один раз.

    // 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);

Ответ 1

В случае сомнений испытайте! Используя jmh, я получаю следующие результаты в списке из 100 тыс. Элементов (в микросекундах, меньше - лучше):

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

Итак, у вас есть от более быстрого до более медленного:

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

Обратите внимание, что результаты очень сильно зависят от оптимизаций JIT. Если логика в сопоставлении более сложна, некоторые из оптимизаций могут быть недоступны (более длинный код = меньше вложения), и в этом случае версии потоков могут занимать 4-5 раз больше времени, чем цикл for, но если эта логика является тяжелой CPU, разница снова уменьшится. Профилирование вашего фактического приложения даст вам дополнительную информацию.


Код для сравнения:

@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();
  }
}

Ответ 2

Я ожидаю, что второй будет самым быстрым.

В боксе нет ни второго, ни третьего примера (если в списке есть уже вложенные элементы). Но есть unboxing.

В вашем втором примере может быть два unboxing (один для каждого x в x*x), а третий делает unboxing только один раз. Тем не менее, распаковка выполняется быстро, и я думаю, что это не стоит оптимизировать, поскольку более длинный конвейер с дополнительным вызовом функции, безусловно, замедлит его.

Sidenote: в общем случае вы не должны ожидать, что Stream будет быстрее, чем регулярные итерации в массивах или списках. Когда вы выполняете математические вычисления, когда скорость имеет значение (например, это), лучше идти в другую сторону: просто перебирайте элементы. Если ваш результат является агрегированным значением, тогда агрегируйте его, если это сопоставление, затем выделите новый массив или список того же размера и заполните его вычисленными значениями.

Ответ 3

Метод mapToInt(), вариант операции карты (такие варианты, как mapToInt(), mapToDouble() и т.д., Создают специализированные для типов потоки, такие как IntStream и DoubleStream). Всякий раз, когда нам нужно использовать какой-либо метод класса IntStream после отображения потока, мы можем использовать mapToINT().