2017-01-12 2 views
4

С целью объединения двух наборов данных в потоке.Производительность для Java Stream.concat VS Collection.addAll

Stream.concat(stream1, stream2).collect(Collectors.toSet()); 

Или

stream1.collect(Collectors.toSet()) 
     .addAll(stream2.collect(Collectors.toSet())); 

Что является более эффективным и почему?

+5

Какие данные собраны в вашем собственном тестировании? – Omaha

+0

Первый теоретически может лучше использовать параллелизм. Но разве у вас есть миллионы элементов, кого это волнует? –

+0

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

ответ

6

Для удобства чтения и намерения Stream.concat(a, b).collect(toSet()) является более понятным, чем второй вариант.

Во имя вопроса «, что является самым эффективным», здесь тест JMH (я бы хотел сказать, что я не использую JMH так много, может быть, есть место для улучшить свой эталонный тест):

Использование JMH, с помощью следующего кода:

package stackoverflow; 

import java.util.HashSet; 
import java.util.Set; 
import java.util.concurrent.TimeUnit; 
import java.util.stream.Collectors; 
import java.util.stream.Stream; 

import org.openjdk.jmh.annotations.Benchmark; 
import org.openjdk.jmh.annotations.BenchmarkMode; 
import org.openjdk.jmh.annotations.Fork; 
import org.openjdk.jmh.annotations.Measurement; 
import org.openjdk.jmh.annotations.Mode; 
import org.openjdk.jmh.annotations.OutputTimeUnit; 
import org.openjdk.jmh.annotations.Scope; 
import org.openjdk.jmh.annotations.Setup; 
import org.openjdk.jmh.annotations.State; 
import org.openjdk.jmh.annotations.Warmup; 
import org.openjdk.jmh.infra.Blackhole; 

@State(Scope.Benchmark) 
@Warmup(iterations = 2) 
@Fork(1) 
@Measurement(iterations = 10) 
@OutputTimeUnit(TimeUnit.NANOSECONDS) 
@BenchmarkMode({ Mode.AverageTime}) 
public class StreamBenchmark { 
    private Set<String> s1; 
    private Set<String> s2; 

    @Setup 
    public void setUp() { 
    final Set<String> valuesForA = new HashSet<>(); 
    final Set<String> valuesForB = new HashSet<>(); 
    for (int i = 0; i < 1000; ++i) { 
     valuesForA.add(Integer.toString(i)); 
     valuesForB.add(Integer.toString(1000 + i)); 
    } 
    s1 = valuesForA; 
    s2 = valuesForB; 
    } 

    @Benchmark 
    public void stream_concat_then_collect_using_toSet(final Blackhole blackhole) { 
    final Set<String> set = Stream.concat(s1.stream(), s2.stream()).collect(Collectors.toSet()); 
    blackhole.consume(set); 
    } 

    @Benchmark 
    public void s1_collect_using_toSet_then_addAll_using_toSet(final Blackhole blackhole) { 
    final Set<String> set = s1.stream().collect(Collectors.toSet()); 
    set.addAll(s2.stream().collect(Collectors.toSet())); 
    blackhole.consume(set); 
    } 
} 

Вы получаете эти результаты (я пропустил какую-то часть для удобства чтения).

Result "s1_collect_using_toSet_then_addAll_using_toSet": 
    156969,172 ±(99.9%) 4463,129 ns/op [Average] 
    (min, avg, max) = (152842,561, 156969,172, 161444,532), stdev = 2952,084 
    CI (99.9%): [152506,043, 161432,301] (assumes normal distribution) 

Result "stream_concat_then_collect_using_toSet": 
    104254,566 ±(99.9%) 4318,123 ns/op [Average] 
    (min, avg, max) = (102086,234, 104254,566, 111731,085), stdev = 2856,171 
    CI (99.9%): [99936,443, 108572,689] (assumes normal distribution) 
# Run complete. Total time: 00:00:25 

Benchmark              Mode Cnt  Score  Error Units 
StreamBenchmark.s1_collect_using_toSet_then_addAll_using_toSet avgt 10 156969,172 ± 4463,129 ns/op 
StreamBenchmark.stream_concat_then_collect_using_toSet   avgt 10 104254,566 ± 4318,123 ns/op 

версия с использованием Stream.concat(a, b).collect(toSet()) должен работать быстрее (если я хорошо читать номера JMH).

С другой стороны, я думаю, что этот результат является нормальным, потому что вы не создать промежуточный набор (это имеет некоторую цену, даже с HashSet), и, как говорится в комментарии первого ответа, Stream является лениво сцепляются.

Используя профилировщик, вы можете увидеть, в какой части он медленнее. Вы также можете использовать toCollection(() -> new HashSet(1000)) вместо toSet(), чтобы узнать, не лежит ли проблема в создании внутреннего массива хеша HashSet.

+0

Спасибо за все усилия! – alan7678

+0

Добро пожаловать. – NoDataFound

1

Невозможно рассказать фронт без теста, но подумайте об этом: если есть много дубликатов, тогда Stream.concat(stream1, stream2) должен создать большой объект, который должен быть создан, потому что вы являетесь каллигатором .collect().

Затем .toSet() должен сравнивать каждое возникновение с каждым предыдущим, возможно, с быстрой функцией хэширования, но все же может иметь много элементов.

С другой стороны, stream1.collect(Collectors.toSet()) .addAll(stream2.collect(Collectors.toSet())) создаст два небольших набора, а затем объединит их.

Объем памяти этого второго варианта потенциально меньше первого.

Edit:

я снова это после прочтения @NoDataFound тест. В более сложной версии теста, действительно, Stream.concat, похоже, работает быстрее, чем Collection.addAll. Я попытался учесть, сколько элементов есть и насколько велики исходные потоки. Я также взял из измерения время, необходимое для создания входных потоков из наборов (что в любом случае ничтожно мало). Вот пример того, как я получаю код ниже.

Concat-collect 10000 elements, all distinct: 7205462 nanos 
Collect-addAll 10000 elements, all distinct: 12130107 nanos 

Concat-collect 100000 elements, all distinct: 78184055 nanos 
Collect-addAll 100000 elements, all distinct: 115191392 nanos 

Concat-collect 1000000 elements, all distinct: 555265307 nanos 
Collect-addAll 1000000 elements, all distinct: 1370210449 nanos 

Concat-collect 5000000 elements, all distinct: 9905958478 nanos 
Collect-addAll 5000000 elements, all distinct: 27658964935 nanos 

Concat-collect 10000 elements, 50% distinct: 3242675 nanos 
Collect-addAll 10000 elements, 50% distinct: 5088973 nanos 

Concat-collect 100000 elements, 50% distinct: 389537724 nanos 
Collect-addAll 100000 elements, 50% distinct: 48777589 nanos 

Concat-collect 1000000 elements, 50% distinct: 427842288 nanos 
Collect-addAll 1000000 elements, 50% distinct: 1009179744 nanos 

Concat-collect 5000000 elements, 50% distinct: 3317183292 nanos 
Collect-addAll 5000000 elements, 50% distinct: 4306235069 nanos 

Concat-collect 10000 elements, 10% distinct: 2310440 nanos 
Collect-addAll 10000 elements, 10% distinct: 2915999 nanos 

Concat-collect 100000 elements, 10% distinct: 68601002 nanos 
Collect-addAll 100000 elements, 10% distinct: 40163898 nanos 

Concat-collect 1000000 elements, 10% distinct: 315481571 nanos 
Collect-addAll 1000000 elements, 10% distinct: 494875870 nanos 

Concat-collect 5000000 elements, 10% distinct: 1766480800 nanos 
Collect-addAll 5000000 elements, 10% distinct: 2721430964 nanos 

Concat-collect 10000 elements, 1% distinct: 2097922 nanos 
Collect-addAll 10000 elements, 1% distinct: 2086072 nanos 

Concat-collect 100000 elements, 1% distinct: 32300739 nanos 
Collect-addAll 100000 elements, 1% distinct: 32773570 nanos 

Concat-collect 1000000 elements, 1% distinct: 382380451 nanos 
Collect-addAll 1000000 elements, 1% distinct: 514534562 nanos 

Concat-collect 5000000 elements, 1% distinct: 2468393302 nanos 
Collect-addAll 5000000 elements, 1% distinct: 6619280189 nanos 

Код

import java.util.HashSet; 
import java.util.Random; 
import java.util.Set; 
import java.util.stream.Collectors; 
import java.util.stream.Stream; 

public class StreamBenchmark { 
    private Set<String> s1; 
    private Set<String> s2; 

    private long createStreamsTime; 
    private long concatCollectTime; 
    private long collectAddAllTime; 

    public void setUp(final int howMany, final int distinct) { 
     final Set<String> valuesForA = new HashSet<>(howMany); 
     final Set<String> valuesForB = new HashSet<>(howMany); 
     if (-1 == distinct) { 
      for (int i = 0; i < howMany; ++i) { 
       valuesForA.add(Integer.toString(i)); 
       valuesForB.add(Integer.toString(howMany + i)); 
      } 
     } else { 
      Random r = new Random(); 
      for (int i = 0; i < howMany; ++i) { 
       int j = r.nextInt(distinct); 
       valuesForA.add(Integer.toString(i)); 
       valuesForB.add(Integer.toString(distinct + j)); 
      } 
     } 
     s1 = valuesForA; 
     s2 = valuesForB; 
    } 

    public void run(final int streamLength, final int distinctElements, final int times, boolean discard) { 
     long startTime; 
     setUp(streamLength, distinctElements); 
     createStreamsTime = 0l; 
     concatCollectTime = 0l; 
     collectAddAllTime = 0l; 
     for (int r = 0; r < times; r++) { 
      startTime = System.nanoTime(); 
      Stream<String> st1 = s1.stream(); 
      Stream<String> st2 = s2.stream(); 
      createStreamsTime += System.nanoTime() - startTime; 
      startTime = System.nanoTime(); 
      Set<String> set1 = Stream.concat(st1, st2).collect(Collectors.toSet()); 
      concatCollectTime += System.nanoTime() - startTime; 
      st1 = s1.stream(); 
      st2 = s2.stream(); 
      startTime = System.nanoTime(); 
      Set<String> set2 = st1.collect(Collectors.toSet()); 
      set2.addAll(st2.collect(Collectors.toSet())); 
      collectAddAllTime += System.nanoTime() - startTime; 
     } 
     if (!discard) { 
      // System.out.println("Create streams "+streamLength+" elements, 
      // "+distinctElements+" distinct: "+createStreamsTime+" nanos"); 
      System.out.println("Concat-collect " + streamLength + " elements, " + (distinctElements == -1 ? "all" : String.valueOf(100 * distinctElements/streamLength) + "%") + " distinct: " + concatCollectTime + " nanos"); 
      System.out.println("Collect-addAll " + streamLength + " elements, " + (distinctElements == -1 ? "all" : String.valueOf(100 * distinctElements/streamLength) + "%") + " distinct: " + collectAddAllTime + " nanos"); 
      System.out.println(""); 
     } 
    } 

    public static void main(String args[]) { 
     StreamBenchmark test = new StreamBenchmark(); 
     final int times = 5; 
     test.run(100000, -1, 1, true); 
     test.run(10000, -1, times, false); 
     test.run(100000, -1, times, false); 
     test.run(1000000, -1, times, false); 
     test.run(5000000, -1, times, false); 
     test.run(10000, 5000, times, false); 
     test.run(100000, 50000, times, false); 
     test.run(1000000, 500000, times, false); 
     test.run(5000000, 2500000, times, false); 
     test.run(10000, 1000, times, false); 
     test.run(100000, 10000, times, false); 
     test.run(1000000, 100000, times, false); 
     test.run(5000000, 500000, times, false); 
     test.run(10000, 100, times, false); 
     test.run(100000, 1000, times, false); 
     test.run(1000000, 10000, times, false); 
     test.run(5000000, 50000, times, false); 
    } 
} 
+0

Из-за вашего первого предложения половины я не делаю нисходящего ответа, но в основном это такая же * преждевременная оптимизация * ОП делает ... –

+2

Согласно javadoc, 'Stream.concat (stream1, stream2)' лениво сцепляются *. – NoDataFound

+0

Что вы подразумеваете под «' Stream.concat (stream1, stream2) 'должен создать большой объект"? Обе операции производят тот же результат 'Set', и кроме того, в' Stream.concat' нет «большого объекта». – Holger

1

Используйте либо.

Если профиль вашего приложения, и этот раздел кода является узким местом, а затем рассмотреть профилирование приложения с различными реализациями и использовать тот, который лучше всего работает

3

Ваш вопрос известен как premature optimization. Никогда не выбирайте один синтаксис над другим только потому, что вы думаете быстрее. Всегда используйте синтаксис, который лучше всего выражает ваше намерение и поддерживает понимание вашей логики.


Вы ничего не знаете о задаче я работаю - alan7678

это правда.

Но мне это не нужно.

Есть два основных сценария:

  1. Вы развиваете OLTP приложения. В этом случае приложение должно отвечать в течение секунды или меньше. Пользователь не будет испытывать разницу в производительности между предложенными вами вариантами.

  2. Вы разрабатываете своего рода batch processing, который будет работать в автономном режиме. В этом случае разница в производительности «может» быть важна, но только если вы платите за то время, когда выполняется пакетный процесс.

В любом случае: проблемы реальной производительности (где вы ускоряющие вам приложение по мультипликаторам, а не по фракциям), как правило, вызваны логикой вы реализованной (например: чрезмерное общение, «скрытые петли» или чрезмерного создание объекта).
Эти проблемы обычно не могут быть решены или предотвращены путем выбора определенного синтаксиса.

Если вы опускаете читаемость для повышения производительности, вы делаете приложение более сложным для обслуживания.
И смена жесткой базы данных кода легко сжигает много денег, которые могут быть сохранены из-за более высокой скорости программ в течение всего срока службы приложения, используя менее читаемый, но немного более быстрый синтаксис.

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

Несомненно, людям любопытно.

К счастью для меня синтаксис, который я предпочитаю, кажется, лучше работает. - alan7678

Если вы знаете, почему вы спросили?

И не могли бы вы так любезно поделиться своими результатами измерений вместе со своей измерительной установкой?

И что более важно: это будет справедливо с Java9 или Java10?

Производительность Javas в основном зависит от реализации JVM, и это может быть изменено. Причина в том, что новые конструкторы синтаксиса (как потоки java) имеют лучшие шансы, что новые версии Java повысят производительность. Но нет гарантии ...

В моем случае потребность в производительности больше, чем разница в читаемости. - alan7678

Вы по-прежнему несете ответственность за это применение через 5 лет? Или вы являетесь консультантом, которому выплачивается начало проекта, а затем переходите к следующему?

У меня никогда не было проекта, где я мог бы решить свои проблемы с производительностью на уровне синтаксиса.
Но я постоянно работаю с устаревшим кодом, который существует 10+ лет, и это трудно поддерживать, потому что кто-то не соблюдает читаемость.

Таким образом, ваш отказ от ответа не распространяется на меня. - alan7678

Это свободный мир, возьмите вас.

+1

Вы ничего не знаете о задаче, над которой я работаю, и, без сомнения, этот вопрос будет иметь значение и в некоторых случаях для других людей. К счастью для меня синтаксис, который я предпочитаю, кажется, тоже работает лучше. В моем случае потребность в производительности больше, чем разница в читаемости. Поэтому ваш не-ответ не относится ко мне. – alan7678

+2

Я оптимизирую алгоритм flink и наткнулся на эту проблему, где это очень актуально. – machete

3

Прежде всего, следует подчеркнуть, что второй вариант: . Коллекционер toSet() возвращает Set с “no guarantees on the type, mutability, serializability, or thread-safety”. Если изменчивость не гарантируется, неверно вызывать addAll в результате Set.

Служит для работы с текущей версией эталонной реализации, где будет создан HashSet, но может перестать работать в будущей версии или альтернативных реализациях. Чтобы исправить это, вы должны заменить toSet() на toCollection(HashSet::new) для работы первого потока collect.

Это приводит к тому, что второй вариант не только менее эффективен с текущей реализацией, как показано в this answer, но также может препятствовать будущей оптимизации, сделанной для коллектора toSet(), настаивая на том, что результат точного типа HashSet. Кроме того, в отличие от коллектора toSet(), коллекционер toCollection(…) не может обнаружить, что целевая коллекция неупорядочена, что может иметь значимость в будущих реализациях.

Смежные вопросы