Существует ли какое-либо преимущество вызова карты после 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);
2 ответа

В случае сомнений испытайте! Используя 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();
 }
}
</integer>


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

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

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

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

licensed under cc by-sa 3.0 with attribution.