2015-05-18 1 views
25

Я пишу библиотеку для начинающих программистов, поэтому я стараюсь, чтобы API был как можно более чистым.Почему примитивный поток не собирает (Collector)?

Одна из вещей, которые должна выполнять моя библиотека, - это выполнить некоторые сложные вычисления в большой коллекции ints или longs. Существует множество сценариев и бизнес-объектов, которые мои пользователи должны вычислить из этих значений, поэтому я подумал, что лучший способ - использовать потоки, чтобы позволить пользователям сопоставлять бизнес-объекты с IntStream или LongStream, а затем вычислять вычисления внутри коллектора.

Однако IntStream и LongStream только параметр 3 собрать метод:

collect(Supplier<R> supplier, ObjIntConsumer<R> accumulator, BiConsumer<R,R> combiner) 

И не имеет collect(Collector) метод проще, что Stream<T> имеет.

Таким образом, вместо того, чтобы быть в состоянии сделать

Collection<T> businessObjs = ... 
MyResult result = businessObjs.stream() 
           .mapToInt(...) 
           .collect(new MyComplexComputation(...)); 

я должен сделать предоставить Поставщик, аккумуляторы и комбайнер, как это:

MyResult result = businessObjs.stream() 
           .mapToInt(...) 
           .collect( 
           ()-> new MyComplexComputationBuilder(...), 
            (builder, v)-> builder.add(v), 
            (a,b)-> a.merge(b)) 
           .build(); //prev collect returns Builder object 

Это слишком сложно для моих начинающих пользователей и очень ошибка склонна.

Моя работа вокруг, чтобы сделать статические методы, которые принимают IntStream или LongStream в качестве входных данных и скрывающие создание коллектора и выполнение для вас

public static MyResult compute(IntStream stream, ...){ 
     return .collect( 
         ()-> new MyComplexComputationBuilder(...), 
         (builder, v)-> builder.add(v), 
         (a,b)-> a.merge(b)) 
       .build(); 
} 

Но это не соблюдаются обычные условности работы с потоками:

IntStream tmpStream = businessObjs.stream() 
           .mapToInt(...); 

MyResult result = MyUtil.compute(tmpStream, ...); 

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

Есть ли более чистый способ сделать это, продолжая работать с IntStream или LongStream?

+0

К сожалению, мой совет будет использовать 'Stream '. Вы можете получить его из 'IntStream'' mapToObj (Function.identity()) '. –

+0

@DmitryGinzburg В потоке потенциально будет много тысяч элементов, и вычисление будет довольно сложным, я не хочу, чтобы вас наказывали все бокс/unboxing – dkatzel

+1

, компилятор может устранить бокс/распаковку, если он может встроить путь кода от конверсии к вашим потребителям. Просто напишите их с помощью интерфейсов * int *, как и с 'IntStream', и посмотрите, генерирует ли он какой-либо мусор или нет. – the8472

ответ

21

Мы фактически создали прототип Collector.OfXxx специализаций. То, что мы нашли - в дополнение к очевидной досаде более специализированных типов - заключалось в том, что это было не очень полезно без полного набора примитивно-специализированных коллекций (например, Trove или GS-Collections, но которые JDK делает не иметь). Без IntArrayList, например, Collector.OfInt просто толкает бокс где-то в другом месте - от коллектора до контейнера, который не имеет большого выигрыша и больше поверхности API.

+0

Итак, вы не думаете, что будет разница в производительности? Я не знал, что будет какой-нибудь бокс, если бы я использовал «IntStream» – dkatzel

+7

. Вы смотрите на неправильный конец потока. IntStream не делает бокса при манипуляции с ints. Но какие результаты контейнеры вы можете положить ints без бокса? Не ArrayList , или HashSet или ... Collector.OfInt не полезен без богатого набора дружественных вещей для сбора. –

+0

Я думаю, что мои утилиты будут в основном использовать функции отображения, которые возвращают ints, такие как 'stream.mapToInt (String :: getLength)' там не должно быть никакого бокса с этой целью – dkatzel

2

Преобразование примитивных потоков в потоки объектов в штучной упаковке, если есть методы, которые вам не хватает.

MyResult result = businessObjs.stream() 
          .mapToInt(...) 
          .boxed() 
          .collect(new MyComplexComputation(...)); 

Или не использовать примитивные потоки в первую очередь, и работать с Integer сек все время.

MyResult result = businessObjs.stream() 
          .map(...)  // map to Integer not int 
          .collect(new MyComplexComputation(...)); 
0

Г-н Geotz provided the definitive answer for why the decision was made not to include specialized Collectors, однако, я хотел бы дополнительно исследовать, насколько это решение повлияло на производительность.

Я думал, что отправлю результаты в качестве ответа.

Я использовал jmh microbenchmark framework времени, сколько времени требуется для вычисления расчетов с использованием обоих видов Коллекторы над коллекциями размеров 1, 100, 1000, 100000 и 1000000:

@BenchmarkMode(Mode.AverageTime) 
@OutputTimeUnit(TimeUnit.NANOSECONDS) 
@State(Scope.Thread) 
public class MyBenchmark { 

@Param({"1", "100", "1000", "100000", "1000000"}) 
public int size; 

List<BusinessObj> seqs; 

@Setup 
public void setup(){ 
    seqs = new ArrayList<BusinessObj>(size); 
    Random rand = new Random(); 
    for(int i=0; i< size; i++){ 
     //these lengths are random but over 128 so no caching of Longs 
     seqs.add(BusinessObjFactory.createOfRandomLength()); 
    } 
} 
@Benchmark 
public double objectCollector() {  

    return seqs.stream() 
       .map(BusinessObj::getLength) 
       .collect(MyUtil.myCalcLongCollector()) 
       .getAsDouble(); 
} 

@Benchmark 
public double primitiveCollector() { 

    LongStream stream= seqs.stream() 
            .mapToLong(BusinessObj::getLength); 
    return MyUtil.myCalc(stream)   
         .getAsDouble(); 
} 

public static void main(String[] args) throws RunnerException{ 
    Options opt = new OptionsBuilder() 
         .include(MyBenchmark.class.getSimpleName()) 
         .build(); 

    new Runner(opt).run(); 
} 

} 

Вот результаты:

# JMH 1.9.3 (released 4 days ago) 
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_31.jdk/Contents/Home/jre/bin/java 
# VM options: <none> 
# Warmup: 20 iterations, 1 s each 
# Measurement: 20 iterations, 1 s each 
# Timeout: 10 min per iteration 
# Threads: 1 thread, will synchronize iterations 
# Benchmark mode: Average time, time/op 
# Benchmark: org.sample.MyBenchmark.objectCollector 

# Run complete. Total time: 01:30:31 

Benchmark      (size) Mode Cnt   Score   Error Units 
MyBenchmark.objectCollector   1 avgt 200  140.803 ±  1.425 ns/op 
MyBenchmark.objectCollector   100 avgt 200  5775.294 ±  67.871 ns/op 
MyBenchmark.objectCollector  1000 avgt 200  70440.488 ± 1023.177 ns/op 
MyBenchmark.objectCollector  100000 avgt 200 10292595.233 ± 101036.563 ns/op 
MyBenchmark.objectCollector  1000000 avgt 200 100147057.376 ± 979662.707 ns/op 
MyBenchmark.primitiveCollector  1 avgt 200  140.971 ±  1.382 ns/op 
MyBenchmark.primitiveCollector  100 avgt 200  4654.527 ±  87.101 ns/op 
MyBenchmark.primitiveCollector  1000 avgt 200  60929.398 ± 1127.517 ns/op 
MyBenchmark.primitiveCollector 100000 avgt 200 9784655.013 ± 113339.448 ns/op 
MyBenchmark.primitiveCollector 1000000 avgt 200 94822089.334 ± 1031475.051 ns/op 

Как вы можете видеть, версия примитивного потока немного быстрее, но даже если в коллекции насчитывается 1 миллион элементов, то это всего лишь 0,05 секунды быстрее (в среднем).

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

Спасибо всем, кто пролил понимание этой проблемы.

+1

Это зависит от задачи, которую вы решаете. В вашем случае, вероятно, ваши вычисления довольно сложны, поэтому накладные расходы бокса не значительны. Я почти реализовал примитивные коллекторы в своей библиотеке, и повышение производительности может быть весьма значительным с 30% на задание «группировка по последней цифре», до 2x при соединении строк и 5х на задаче «сумма по знаку». См. Мои результаты и результаты [здесь] (https://gist.github.com/amaembo/fe03b2944cbb6e621158). –

+0

@TagirValeev да, вы, вероятно, правы. Большая часть времени проводится внутри моего вычисления, которое уже распаколо все. – dkatzel

+2

@dkatzel Попробуйте измерить разницу в {скорости, масштабируемости} между 'IntStream.sum()' и 'Stream .reduce (0, Integer :: sum, Integer :: сумма) '. Вы увидите, что в этих случаях разница в скорости и параллельном ускорении огромна - поэтому примитивные специализации были оправданы для основных операций. Но по мере того как операции становятся более тяжеловесными, преимущество ниже, а бокс становится более приемлемым. –

3

Я реализовал примитивные коллекторы в своей библиотеке StreamEx (начиная с версии 0.3.0). Существуют интерфейсы IntCollector, LongCollector и DoubleCollector, которые расширяют интерфейс Collector и специализируются на работе с примитивами. Существует еще одна небольшая разница в процедуре объединения, так как методы, такие как IntStream.collect, принимают BiConsumer вместо BinaryOperator.

Существует множество предопределенных методов сбора, чтобы объединить числа в строку, сохранить в примитивный массив, до BitSet, найти минимум, суммировать, суммировать статистику, выполнять групповые операции и операции разделения. Конечно, вы можете определить своих коллекционеров. Вот несколько примеров использования (предполагается, что у вас есть массив int[] input с входными данными).

Регистрация числа как строки с разделителем:

String nums = IntStreamEx.of(input).collect(IntCollector.joining(",")); 

Группировка по последней цифре:

Map<Integer, int[]> groups = IntStreamEx.of(input) 
     .collect(IntCollector.groupingBy(i -> i % 10)); 

Сумма положительных и отрицательных чисел в отдельности:

Map<Boolean, Integer> sums = IntStreamEx.of(input) 
     .collect(IntCollector.partitioningBy(i -> i > 0, IntCollector.summing())); 

Вот простой benchmark, который сравнивает этих коллекционеров и обычных коллекционеров объектов.

Обратите внимание, что моя библиотека не предоставляет (и не предоставляет в будущем) любые видимые пользователем структуры данных, такие как карты на примитивах, поэтому группировка выполняется в обычном HashMap. Однако, если вы используете Trove/GS/HFTC/что-то еще, не так сложно написать дополнительные примитивные коллекторы для структур данных, определенных в этих библиотеках, чтобы повысить производительность.

6

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

MyResult result = businessObjs.stream() 
           .mapToInt(...) 
           .collect( 
            MyComplexComputationBuilder::new, 
            MyComplexComputationBuilder::add, 
            MyComplexComputationBuilder::merge) 
           .build(); //prev collect returns Builder object 

В Брайаных definitive answer to this question, он упоминает о двух других структурах сбора Java, которые делают имеют примитивные коллекции, которые на самом деле могут быть использованы с сборным методом на примитивных потоках. Я подумал, что было бы полезно проиллюстрировать некоторые примеры использования примитивных контейнеров в этих рамках с примитивными потоками. Код ниже также будет работать с параллельным потоком.

// Eclipse Collections 
List<Integer> integers = Interval.oneTo(5).toList(); 

Assert.assertEquals(
     IntInterval.oneTo(5), 
     integers.stream() 
       .mapToInt(Integer::intValue) 
       .collect(IntArrayList::new, IntArrayList::add, IntArrayList::addAll)); 

// Trove Collections 

Assert.assertEquals(
     new TIntArrayList(IntStream.range(1, 6).toArray()), 
     integers.stream() 
       .mapToInt(Integer::intValue) 
       .collect(TIntArrayList::new, TIntArrayList::add, TIntArrayList::addAll)); 

Примечание: Я коммиттер для Eclipse Collections.