I think documentation could help this (and maybe removing the two added
`of` helpers so an explicit collector must be created), but you're right
about the cost. If the consensus is not to change Collector, then maybe
Tagir's suggestion for internal-only change can be implemented?

On Mon, Mar 4, 2019 at 2:53 PM Brian Goetz <brian.go...@oracle.com> wrote:

> So, unfortunately I think this particular API evolution idiom is kind of
> questionable.  There is a new method, sizedSupplier, with a default that
> delegates to the unsized supplier.  But, that leaves implementors in a
> position where they don’t really know which one to implement, and the use
> of characteristics to hint at which one to call is kind of convoluted.
>
> You’re focused on the benefits — that in some cases, collection can be
> faster.  That’s good.  But one of the big costs here is that an interfaces
> that was simple is now considerably less so.  That’s a cost; not convinced
> the two are in balance.
>
>
> On Mar 4, 2019, at 12:35 AM, August Nagro <augustna...@gmail.com> wrote:
>
> Hi everyone,
>
> My implementation is at
> https://github.com/AugustNagro/presize-collectors-bench and I have a
> webrev here: http://august.nagro.us/presized-collectors/webrev/.
>
> The changes that I made were:
>
>    - Add `default IntFunction<A> sizedSupplier()` to Collector
>    - Add 2 new Collector.of helper methods taking a sized supplier
>    - Update the applicable collectors in Collectors to provide a
>    sizedSupplier
>    - Have ReferencePipeline & ReduceOps call sizedSupplier when
>    appropriate
>
> Some notes/questions I have:
>
>    - I considered adding Collector.Characteristics.PRESIZEABLE, but
>    figured it was not needed given that sizedSupplier should always delegate
>    to supplier.
>    - Collector.sizedSupplier could be a LongFunction instead of
>    IntFunction (since Sink.begin provides long sizes), but every Collection
>    type I've looked at uses int for initialCapacity, so I think IntFunction
>    makes more sense.
>    - StringJoiner should be updated to take an initalCapacity (should I
>    commit this change in the webrev, or leave for another enhancement?)
>    - What tests should I add for this enhancement?
>
> There are some benchmarks in the github repo. I was initially surprised
> when I saw that my bare-bones parallel stream did not have a throughput
> improvement like the serial one, but after doing some debugging I think it
> is from Collector.combiner dominating the runtime.
>
> Regards,
>
> August
>
> On Thu, Feb 28, 2019 at 12:56 AM Tagir Valeev <amae...@gmail.com> wrote:
>
>> Hello!
>>
>> I wouldn't use presized HashSet, because you never know how many
>> duplicates are expected. What if the input stream has a million of
>> elements, but only two of them are distinct? Do you really want to allocate
>> a hash table for million elements in advance?
>>
>> For toMap() without custom supplier and merger preallocation sounds
>> reasonable as in this case duplicating key is an exceptional case, so we
>> may expect a specific number of elements.
>>
>> чт, 28 февр. 2019 г., 11:38 August Nagro augustna...@gmail.com:
>>
>>> Tagir,
>>>
>>> Great to see the validating benchmarks.
>>>
>>> > I think it's the best solution given the fact that very few collectors may
>>> benefit from the exact known size, and this benefit usually disappears
>>> when collectors are composed (e.g. using groupingBy: downstream
>>> toList() would not win anything if it provides a sizedSupplier).
>>>
>>> I would like to see your benchmarks for that statement too. After all,
>>> Hashmap, HashSet, etc all have presizing constructors, aren't those there
>>> for a reason?
>>>
>>> So I am still convinced that presizing is important for other collector
>>> types, including custom collectors. Although I'm open to having my mind
>>> changed.
>>>
>>>  I'm happy to contribute an implementation as well, I was hoping that
>>> this this (smallish) patch would help me learn the OpenJdk process and make
>>> future contributions easier!
>>>
>>> - August
>>>
>>> On Wed, Feb 27, 2019 at 1:06 AM Tagir Valeev <amae...@gmail.com> wrote:
>>>
>>>> Hello!
>>>>
>>>> > A less intrusive API direction might be a version of Collector whose
>>>> > supplier function took a size-estimate argument; this might even help
>>>> in
>>>> > parallel since it allows for intermediate results to start with a
>>>> better
>>>> > initial size guess.  (And this could be implemented as a default that
>>>> > delegates to the existing supplier.)  Still, not really sure this
>>>> > carries its weight.
>>>>
>>>> It's interesting that such optimization is possible without API
>>>> change, using the "hidden knowledge".
>>>> While public API stays the same, the collect method implementation may
>>>> check if custom
>>>> non-public Collector implementation is supplied, and in this case may
>>>> use the sized supplier.
>>>> I think it's the best solution given the fact that very few collectors
>>>> may benefit from the exact
>>>> known size, and this benefit usually disappears when collectors are
>>>> composed (e.g. using groupingBy:
>>>> downstream toList() would not win anything if it provides a
>>>> sizedSupplier).
>>>>
>>>> I created a quick patch and launched a quick benchmark like this:
>>>> return new SplittableRandom(0).ints(length, 0,
>>>> 100).boxed().collect(Collectors.toList());
>>>> For length = 100, 10000, 1000000
>>>>
>>>> Here's results for vanilla 13-ea+8:
>>>> length = 100, avg time = 1434,469 ± 27,147  ns/op, alloc rate = 1712
>>>> B/op
>>>> length = 10000, avg time = 155032,390 ± 2657,927  ns/op, alloc rate =
>>>> 169280 B/op
>>>> length = 1000000, avg time = 27099621,763 ± 1979054,500 ns/op, alloc
>>>> rate = 14586750 B/op
>>>>
>>>> Patched:
>>>> length = 100, avg time = 1414,480 ± 30,040  ns/op, alloc rate = 768 B/op
>>>> length = 10000, avg time = 137338,320 ± 1316,789  ns/op, alloc rate =
>>>> 40368 B/op
>>>> length = 1000000, avg time = 17654635,871 ± 210607,441  ns/op, alloc
>>>> rate = 4000382 B/op
>>>>
>>>> As you can see, exact allocation really helps to reduce alloc pressure
>>>> and produces better performance for big lists.
>>>>
>>>> If such kind of patch is acceptable for Stream API, I can file a
>>>> ticket and submit a webrev.
>>>>
>>>> With best regards,
>>>> Tagir Valeev
>>>>
>>>> The patch follows:
>>>>
>>>> --- src/java.base/share/classes/java/util/stream/Collectors.java
>>>> (revision 53626:e2fc434b410a35b28ab433c29863c8a26e4e813a)
>>>> +++ src/java.base/share/classes/java/util/stream/Collectors.java
>>>> (revision 53626+:e2fc434b410a+)
>>>> @@ -50,6 +50,7 @@
>>>>  import java.util.function.BinaryOperator;
>>>>  import java.util.function.Consumer;
>>>>  import java.util.function.Function;
>>>> +import java.util.function.IntFunction;
>>>>  import java.util.function.Predicate;
>>>>  import java.util.function.Supplier;
>>>>  import java.util.function.ToDoubleFunction;
>>>> @@ -194,23 +195,34 @@
>>>>       */
>>>>      static class CollectorImpl<T, A, R> implements Collector<T, A, R> {
>>>>          private final Supplier<A> supplier;
>>>> +        private final IntFunction<A> sizedSupplier;
>>>>          private final BiConsumer<A, T> accumulator;
>>>>          private final BinaryOperator<A> combiner;
>>>>          private final Function<A, R> finisher;
>>>>          private final Set<Characteristics> characteristics;
>>>>
>>>>          CollectorImpl(Supplier<A> supplier,
>>>> +                      IntFunction<A> sizedSupplier,
>>>>                        BiConsumer<A, T> accumulator,
>>>>                        BinaryOperator<A> combiner,
>>>>                        Function<A,R> finisher,
>>>>                        Set<Characteristics> characteristics) {
>>>>              this.supplier = supplier;
>>>> +            this.sizedSupplier = sizedSupplier;
>>>>              this.accumulator = accumulator;
>>>>              this.combiner = combiner;
>>>>              this.finisher = finisher;
>>>>              this.characteristics = characteristics;
>>>>          }
>>>>
>>>> +        CollectorImpl(Supplier<A> supplier,
>>>> +                      BiConsumer<A, T> accumulator,
>>>> +                      BinaryOperator<A> combiner,
>>>> +                      Function<A,R> finisher,
>>>> +                      Set<Characteristics> characteristics) {
>>>> +            this(supplier, null, accumulator, combiner, finisher,
>>>> characteristics);
>>>> +        }
>>>> +
>>>>          CollectorImpl(Supplier<A> supplier,
>>>>                        BiConsumer<A, T> accumulator,
>>>>                        BinaryOperator<A> combiner,
>>>> @@ -228,6 +240,10 @@
>>>>              return supplier;
>>>>          }
>>>>
>>>> +        IntFunction<A> sizedSupplier() {
>>>> +            return sizedSupplier;
>>>> +        }
>>>> +
>>>>          @Override
>>>>          public BinaryOperator<A> combiner() {
>>>>              return combiner;
>>>> @@ -275,8 +291,11 @@
>>>>       */
>>>>      public static <T>
>>>>      Collector<T, ?, List<T>> toList() {
>>>> -        return new CollectorImpl<>((Supplier<List<T>>)
>>>> ArrayList::new, List::add,
>>>> +        return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new,
>>>> +                                   ArrayList::new,
>>>> +                                   List::add,
>>>>                                     (left, right) -> {
>>>> left.addAll(right); return left; },
>>>> +                                   castingIdentity(),
>>>>                                     CH_ID);
>>>>      }
>>>>
>>>> Index: src/java.base/share/classes/java/util/stream/ReduceOps.java
>>>> IDEA additional info:
>>>> Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
>>>> <+>UTF-8
>>>> ===================================================================
>>>> --- src/java.base/share/classes/java/util/stream/ReduceOps.java
>>>> (revision 53626:e2fc434b410a35b28ab433c29863c8a26e4e813a)
>>>> +++ src/java.base/share/classes/java/util/stream/ReduceOps.java
>>>> (revision 53626+:e2fc434b410a+)
>>>> @@ -36,6 +36,7 @@
>>>>  import java.util.function.BinaryOperator;
>>>>  import java.util.function.DoubleBinaryOperator;
>>>>  import java.util.function.IntBinaryOperator;
>>>> +import java.util.function.IntFunction;
>>>>  import java.util.function.LongBinaryOperator;
>>>>  import java.util.function.ObjDoubleConsumer;
>>>>  import java.util.function.ObjIntConsumer;
>>>> @@ -155,13 +156,20 @@
>>>>      public static <T, I> TerminalOp<T, I>
>>>>      makeRef(Collector<? super T, I, ?> collector) {
>>>>          Supplier<I> supplier =
>>>> Objects.requireNonNull(collector).supplier();
>>>> +        @SuppressWarnings("unchecked")
>>>> +        IntFunction<I> sizedSupplier = collector instanceof
>>>> Collectors.CollectorImpl ?
>>>> +                ((Collectors.CollectorImpl<?, I, ?>)
>>>> collector).sizedSupplier() : null;
>>>>          BiConsumer<I, ? super T> accumulator = collector.accumulator();
>>>>          BinaryOperator<I> combiner = collector.combiner();
>>>>          class ReducingSink extends Box<I>
>>>>                  implements AccumulatingSink<T, I, ReducingSink> {
>>>>              @Override
>>>>              public void begin(long size) {
>>>> -                state = supplier.get();
>>>> +                if (sizedSupplier != null && size >= 0 && size <=
>>>> Integer.MAX_VALUE) {
>>>> +                    state = sizedSupplier.apply((int) size);
>>>> +                } else {
>>>> +                    state = supplier.get();
>>>> +                }
>>>>              }
>>>>
>>>>              @Override
>>>>
>>>
>

Reply via email to