2015-05-18 10 views
25

Sto scrivendo una libreria per programmatori principianti, quindi sto cercando di mantenere l'API più pulita possibile.Perché il flusso primitivo non è stato raccolto (Collector)?

Una delle cose che la mia biblioteca ha bisogno di fare è eseguire alcuni calcoli complessi su una vasta collezione di interi o long. Ci sono molti scenari e oggetti business di cui i miei utenti hanno bisogno per calcolare questi valori, quindi ho pensato che il modo migliore sarebbe utilizzare gli stream per consentire agli utenti di mappare gli oggetti aziendali su IntStream o LongStream e quindi calcolare i calcoli all'interno di un raccoglitore.

Tuttavia IntStream e LongStream hanno solo il parametro 3 metodo di raccogliere:

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

E non ha la collect(Collector) metodo più semplice che Stream<T> ha.

Così, invece di essere in grado di fare

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

devo fare fornire Fornitori, accumulatori e combinatori come questo:

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

Questo è troppo complicato per i miei utenti alle prime armi ed è molto incline a errori.

mio lavoro intorno è quello di rendere i metodi statici che accettano un IntStream o LongStream come input e nascondono la creazione di raccolta e l'esecuzione per voi

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

Ma che non segue le normali convenzioni di lavoro con Streams:

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

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

Perché bisogna o salvare una variabile temporanea e passare che per il metodo statico, o creare il flusso all'interno della chiamata statica che può essere fonte di confusione quando è è mescolato con gli altri parametri di m computazione

Esiste un modo più semplice per farlo mentre si sta ancora lavorando con IntStream o LongStream?

+0

Sfortunatamente, il mio consiglio sarebbe utilizzare 'Stream '. Puoi ottenerlo da 'IntStream' di' mapToObj (Function.identity()) '. –

+0

@DmitryGinzburg il flusso avrà potenzialmente molte migliaia di elementi e il calcolo è piuttosto complesso, non voglio essere penalizzato da tutto il boxing/unboxing – dkatzel

+1

il compilatore potrebbe essere in grado di eliminare il boxing/unboxing se può inline il percorso del codice dalla conversione ai tuoi consumatori. Basta scriverli con le interfacce * int * -based come faresti con 'IntStream' e vedere se genera o meno spazzatura. – the8472

risposta

21

Abbiamo infatti prototipato alcune specializzazioni Collector.OfXxx. Quello che abbiamo trovato - oltre all'ovvio fastidio di tipi più specializzati - era che non era davvero molto utile senza avere un complemento completo di raccolte specializzate primitive (come Trove, o GS-Collections, ma che il JDK fa non avere). Senza una IntArrayList, ad esempio, un Collector.OfInt spinge semplicemente la boxe da qualche altra parte - dal Collector al contenitore - che non ha grandi vincite e molta più superficie API.

+0

Quindi non pensate che ci sarebbe una differenza di prestazioni? Ero inconsapevole che ci sarebbe stato del pugilato se avessi usato un 'IntStream' – dkatzel

+7

Stai guardando la parte sbagliata del flusso. Un IntStream non esegue il pugilato durante la manipolazione di ints. Ma in che modo i contenitori si possono inserire senza la boxe? Not a ArrayList o HashSet , o ... Collector.OfInt non è utile senza un ricco set di cose int-friendly da raccogliere. –

+0

Penso che i miei casi usino principalmente le funzioni di mappatura che restituiscono valori come 'stream.mapToInt (String :: getLength)' non ci dovrebbe essere alcun boxing a tale scopo o – dkatzel

2

Converti i flussi primitivi in ​​flussi di oggetti in scatola se ci sono metodi che ti mancano.

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

O non utilizzare i flussi primitivi, in primo luogo e lavorare con Integer s per tutto il tempo.

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

Mr. Geotz provided the definitive answer for why the decision was made not to include specialized Collectors, tuttavia, volevo indagare ulteriormente su quanto questa decisione influisse sulle prestazioni.

Ho pensato di pubblicare i risultati come risposta.

ho usato il jmh microbenchmark framework in volta quanto tempo ci vuole per calcolare i calcoli usando entrambi i tipi di collezionisti più di collezioni di dimensioni 1, 100, 1000, 100.000 e 1 milione:

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

} 

ecco i risultati:

# 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 

Come si può vedere, la versione primitiva stream è leggermente più veloce, ma anche quando ci sono 1 milione di elementi della collezione, è solo 0,05 secondi più veloce (in media).

Per la mia API preferisco attenermi alle convenzioni Object Stream più pulite e utilizzare la versione Boxed poiché è una penalità di prestazioni minore.

Grazie a tutti coloro che hanno approfondito questo problema.

+1

Dipende dal compito che stai risolvendo. Nel tuo caso probabilmente i tuoi calcoli sono piuttosto complessi, quindi il sovraccarico di boxe non è significativo. Ho quasi implementato i collezionisti primitivi nella mia libreria e l'aumento delle prestazioni può essere piuttosto significativo dal 30% all'attività "Raggruppamento per ultima cifra", al 2x alla stringa di join e 5x al task "somma per segno". Vedi il mio benchmark e risultati [qui] (https://gist.github.com/amaembo/fe03b2944cbb6e621158). –

+0

@TagirValeev sì probabilmente hai ragione. La maggior parte del tempo viene spesa all'interno del mio calcolo che ha già unboxed tutto – dkatzel

+2

@dkatzel Prova a misurare la differenza in {velocità, scalabilità} tra 'IntStream.sum()' e 'Stream .reduce (0, numero intero :: somma, numero intero :: sum) '. Vedrai che in questi casi la differenza di velocità e di accelerazione parallela è enorme, motivo per cui le specializzazioni primitive erano giustificate per le operazioni di base. Ma poiché le operazioni diventano più pesanti, il beneficio è inferiore e la boxe diventa più accettabile. –

3

Ho implementato i collettori primitivi nella mia libreria StreamEx (dalla versione 0.3.0). Ci sono interfacce IntCollector, LongCollector e DoubleCollector che estendono l'interfaccia Collector e sono specializzate per lavorare con le primitive. Esiste un'ulteriore piccola differenza nella combinazione della procedura poiché i metodi come IntStream.collect accettano uno BiConsumer anziché BinaryOperator.

Esistono numerosi metodi di raccolta predefiniti per unire i numeri alla stringa, memorizzare nell'array primitivo, a BitSet, trovare min, max, sum, calcolare le statistiche di riepilogo, eseguire operazioni di raggruppamento e partizione. Certo, puoi definire i tuoi stessi collezionisti. Ecco alcuni esempi di utilizzo (presupponendo che l'array int[] input contenga dati di input).

registrazione numeri come stringa con separatori:

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

Raggruppamento per ultima cifra:

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

Somma numeri positivi e negativi separatamente:

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

Ecco un semplice benchmark che confronta questi collezionisti e soliti collezionisti di oggetti.

Si noti che la mia libreria non fornisce (e non fornirà in futuro) strutture di dati visibili all'utente come le mappe su primitive, pertanto il raggruppamento viene eseguito nel solito HashMap. Tuttavia, se si utilizza Trove/GS/HFTC/qualunque, non è così difficile scrivere ulteriori collettori primitivi per le strutture dati definite in queste librerie per ottenere più prestazioni.

6

Forse se vengono utilizzati i riferimenti al metodo anziché lambda, il codice necessario per il flusso primitivo di raccolta non sembrerà complicato.

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

In Brian definitive answer to this question, cita altri due quadri di raccolta Java che fare avere collezioni primitivi che in realtà possono essere utilizzati con il metodo Collect sui flussi primitive. Ho pensato che potrebbe essere utile illustrare alcuni esempi di come utilizzare i contenitori primitivi in ​​questi framework con flussi primitivi. Il codice seguente funzionerà anche con un flusso parallelo.

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

Nota: Sono un committer per Eclipse Collections.

Problemi correlati