2014-10-28 11 views
12

IDEA suggerisce di sostituire, per esempio, questo:IntelliJ IDEA suggerisce di sostituire i loop con il metodo foreach. Dovrei farlo sempre quando possibile?

for (Point2D vertex : graph.vertexSet()) { 
    union.addVertex(vertex); 
} 

con questo:

graph.vertexSet().forEach(union::addVertex); 

Questa nuova versione è sicuramente molto più leggibile. Ma ci sono situazioni in cui è meglio attenersi al buon costrutto del vecchio linguaggio per le reti iterative piuttosto che usare il nuovo metodo foreach?

Ad esempio, se ho capito bene, il meccanismo di riferimento del metodo implica la costruzione di un oggetto anonimo Consumer che altrimenti (con il costrutto del linguaggio for) non sarebbe stato costruito. Potrebbe diventare un collo di bottiglia per le prestazioni di alcune azioni?

Così ho scritto questo benchmark non molto esaustivo:

package org.sample; 

import org.openjdk.jmh.annotations.Benchmark; 
import org.openjdk.jmh.annotations.Fork; 
import org.openjdk.jmh.annotations.Threads; 
import org.openjdk.jmh.infra.Blackhole; 
import org.tendiwa.geometry.Point2D; 

import java.util.ArrayList; 
import java.util.List; 
import java.util.stream.Collectors; 
import java.util.stream.IntStream; 

public class LanguageConstructVsForeach { 
    private static final int NUMBER_OF_POINTS = 10000; 
    private static final List<Point2D> points = IntStream 
     .range(0, NUMBER_OF_POINTS) 
     .mapToObj(i -> new Point2D(i, i * 2)) 
     .collect(Collectors.toList()); 

    @Benchmark 
    @Threads(1) 
    @Fork(3) 
    public void languageConstructToBlackhole(Blackhole bh) { 
     for (Point2D point : points) { 
      bh.consume(point); 
     } 
    } 
    @Benchmark 
    @Threads(1) 
    @Fork(3) 
    public void foreachToBlackhole(Blackhole bh) { 
     points.forEach(bh::consume); 
    } 
    @Benchmark 
    @Threads(1) 
    @Fork(3) 
    public List<Point2D> languageConstructToList(Blackhole bh) { 
     List<Point2D> list = new ArrayList<>(NUMBER_OF_POINTS); 
     for (Point2D point : points) { 
      list.add(point); 
     } 
     return list; 
    } 
    @Benchmark 
    @Threads(1) 
    @Fork(3) 
    public List<Point2D> foreachToList(Blackhole bh) { 
     List<Point2D> list = new ArrayList<>(NUMBER_OF_POINTS); 
     points.forEach(list::add); 
     return list; 
    } 

} 

e ottenuto:

Benchmark              Mode Samples  Score  Error Units 
o.s.LanguageConstructVsForeach.foreachToBlackhole    thrpt  60 33693.834 ± 894.138 ops/s 
o.s.LanguageConstructVsForeach.foreachToList     thrpt  60 7753.941 ± 239.081 ops/s 
o.s.LanguageConstructVsForeach.languageConstructToBlackhole thrpt  60 16043.548 ± 644.432 ops/s 
o.s.LanguageConstructVsForeach.languageConstructToList   thrpt  60 6499.527 ± 202.589 ops/s 

Come viene foreach è più efficiente in entrambi i casi: quando faccio praticamente nulla e quando lo faccio un po ' lavoro vero? Non foreach semplicemente incapsulare Iterator? Questo benchmark è corretto? Se lo è, c'è qualche ragione oggi per usare il costrutto vecchio linguaggio con Java 8?

+2

Domande correlate: http://stackoverflow.com/q/23265855/1441122 e http://stackoverflow.com/q/16635398/1441122 –

+0

i microbenchmark sono altrettanto accurati e utili quanto i loro esperti nella creazione di essi. i microbenchmark di solito giocano direttamente all'effetto Dunning-Kruger con i risultati attesi. –

+1

@JarrodRoberson Ma questo utilizza JMH, che ha un approccio consolidato e strumenti per il benchamrking. Non c'è niente in questo benchmark che deriva dalla mia esperienza e non dal manuale a JMH. – gvlasov

risposta

15

Si sta confrontando il ciclo "enhanced-for" della lingua con il metodo Iterable.forEach(). Il benchmark non è ovviamente sbagliato, e i risultati potrebbero sembrare sorprendenti, fino a quando non approfondirai le implementazioni.

Si noti che l'elenco points è un'istanza di ArrayList poiché è ciò che viene creato dal raccoglitore Collectors.toList().

Il ciclo enhanced per in un Iterable ottiene un Iterator da esso e poi chiama hasNext() e next() ripetutamente finché non ci sono più elementi. (Questo differisce dal ciclo enhanced-for su un array, che esegue l'accesso aritmetico e diretto all'elemento dell'array.) Quindi, durante il looping su un Iterable, questo ciclo eseguirà almeno due chiamate di metodo per iterazione.

Al contrario, chiamando ArrayList.forEach() si esegue un convenzionale per-loop basato su int sull'array che contiene gli elementi dell'elenco e chiama il lambda una volta per iterazione. C'è solo una chiamata per iterazione qui, in contrapposizione a due chiamate per iterazione per il ciclo enhanced-for. Questo potrebbe spiegare perché ArrayList.forEach() è più veloce in questo caso.

Il caso blackhole sembra fare pochissimo lavoro se non eseguire i loop, quindi questi casi sembrano misurare l'overhead puro. Questo potrebbe essere il motivo per cui lo ArrayList.forEach() mostra un grande vantaggio qui.

Quando il ciclo fa un po 'di lavoro (aggiungendo a una lista di destinazione) c'è ancora un vantaggio di velocità per ArrayList.forEach(), ma è una differenza molto più piccola. Sospetto che se dovessi fare più lavoro all'interno del ciclo, il vantaggio sarebbe ancora più piccolo. Questo dimostra che l'overhead del ciclo per entrambi i costrutti è molto piccolo. Prova a utilizzare BlackHole.consumeCPU() nel ciclo. Non sarei sorpreso se i risultati tra i due costrutti diventassero indistinguibili.

Si noti che il grande vantaggio di velocità si verifica perché Iterable.forEach() termina con un'implementazione specializzata in ArrayList.forEach(). Se dovessi eseguire forEach() su una diversa struttura dati, probabilmente otterrai risultati diversi.

Non lo userei come giustificazione per sostituire ciecamente tutti i cicli enhanced-for con chiamate a Iterable.forEach(). Scrivi il codice che è il più chiaro e che ha più senso. Se stai scrivendo un codice critico per le prestazioni, confrontalo! Forme diverse avranno prestazioni diverse, a seconda del carico di lavoro, della struttura dei dati che viene attraversata, ecc.

2

Un motivo ovvio per utilizzare lo stile precedente è che sarai compatibile con Java 7. Se il tuo codice utilizza molte nuove funzioni I benefici di Java 8 non sono un'opzione, ma se si utilizzano solo alcune nuove funzionalità questo potrebbe essere un vantaggio, soprattutto se il codice è in una libreria di uso generale.

Problemi correlati