Iterador versus Flujo de Java 8
Para aprovechar la amplia gama de métodos de consulta incluidos en java.util.stream
de Jdk 8, intento diseñar modelos de dominio donde los captadores de relación con *
multiplicidad (con cero o más instancias ) devuelven un Stream<T>
, en lugar de un Iterable<T>
o Iterator<T>
.
Mi duda es si hay algún gasto adicional incurrido por el Stream<T>
en comparación con el Iterator<T>
?
Entonces, ¿hay alguna desventaja de comprometer mi modelo de dominio con un Stream<T>
?
O en su lugar, debería siempre devuelve un Iterator<T>
o Iterable<T>
, y deja al usuario final la decisión de elegir si usar un flujo, o no, convirtiendo ese iterador con el StreamUtils
?
Tenga en cuenta que devolver un Collection
no es una opción válida porque en este caso la mayoría de las relaciones son perezosas y con tamaño desconocido.
2 answers
Hay muchos consejos de rendimiento aquí, pero lamentablemente gran parte de ellos son conjeturas, y poco de ello apunta a las consideraciones de rendimiento reales.
@Holger lo hace bien señalando que debemos resistir la tendencia aparentemente abrumadora de dejar que la cola de rendimiento mueva el perro de diseño de API.
Si bien hay un trillón de consideraciones que pueden hacer que una corriente sea más lenta que, lo mismo que, o más rápida que alguna otra forma de recorrido en cualquier caso dado, hay son algunos factores que apuntan a que las transmisiones tienen una ventaja de rendimiento cuando cuentan on en conjuntos de big data.
Hay una sobrecarga de inicio fija adicional de crear un Stream
en comparación con crear un Iterator
a algunos objetos más antes de comenzar a calcular. Si su conjunto de datos es grande, no importa; es un pequeño costo inicial amortizado durante muchos cálculos. (Y si su conjunto de datos es pequeño, probablemente tampoco importa because porque si su programa está funcionando en conjuntos de datos pequeños, el rendimiento generalmente no es su preocupación #1 tampoco.) Donde esto hace importa es cuando va en paralelo; cualquier tiempo dedicado a la configuración de la canalización entra en la fracción serial de la ley de Amdahl; si nos fijamos en la implementación, trabajamos duro para mantener la cuenta regresiva de objetos durante la configuración de flujo, pero me encantaría encontrar formas de reducirlo, ya que tiene un efecto directo en el tamaño del conjunto de datos de equilibrio donde el paralelo comienza a ganar sobre secuencial.
Pero, más importante que el costo de inicio fijo es el costo de acceso por elemento. Aquí, las transmisiones realmente ganan, y a menudo ganan en grande, lo que algunos pueden encontrar sorprendente. (En nuestras pruebas de rendimiento, vemos rutinariamente canalizaciones de flujo que pueden superar a sus contrapartes de bucle for sobre Collection
.) Y, hay una explicación simple para esto: Spliterator
tiene fundamentalmente menores costos de acceso por elemento que Iterator
, incluso secuencialmente. Hay varias razones para esto.
El protocolo Iterador es fundamentalmente menos eficiente. Requiere llamar a dos métodos para obtener cada elemento. Además, debido a que los iteradores deben ser robustos para cosas como llamar
next()
sinhasNext()
, ohasNext()
varias veces sinnext()
, ambos métodos generalmente tienen que hacer algo de codificación defensiva (y generalmente más statefulness y ramificación), lo que se suma a la ineficiencia. Por otro lado, incluso la forma lenta de atravesar un spliterator (tryAdvance
) no tiene esta carga. (Es aún peor para los datos concurrentes estructuras, porque elnext
/hasNext
la dualidad es fundamentalmente picante, y las implementacionesIterator
tienen que hacer más trabajo para defenderse contra modificaciones concurrentes que las implementacionesSpliterator
.)Spliterator
además ofrece una iteración de "ruta rápida" {forEachRemaining
which que se puede usar la mayor parte del tiempo (reduction, forEach), reduciendo aún más la sobrecarga del código de iteración que media el acceso a las estructuras internas de datos. Esto también tiende a encajar muy bien, lo que a su vez aumenta la efectividad de otras optimizaciones como movimiento de código, eliminación de verificación de límites, etc.Además, el recorrido a través de
Spliterator
tiende a tener muchas menos escrituras de montón que conIterator
. ConIterator
, cada elemento causa una o más escrituras de montón (a menos que elIterator
se pueda escalar a través del análisis de escape y sus campos izados en registros.) Entre otros problemas, esto causa actividad de marca de tarjeta GC, lo que lleva a la contención de línea de caché para las marcas de tarjeta. Por otro lado,Spliterators
tienden a tienen menos estado, y las implementaciones de fuerza industrialforEachRemaining
tienden a diferir la escritura de cualquier cosa en el montón hasta el final del recorrido, en lugar de almacenar su estado de iteración en locales que naturalmente se asignan a los registros, lo que resulta en una actividad de bus de memoria reducida.
Resumen: no te preocupes, sé feliz. Spliterator
es mejor Iterator
, incluso sin paralelismo. (También son generalmente más fáciles de escribir y más difíciles de equivocarse.)
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:24:17
Comparemos la operación común de iterar sobre todos los elementos, asumiendo que la fuente es un ArrayList
. Entonces, hay tres maneras estándar de lograr esto:
-
final E[] elementData = (E[]) this.elementData; final int size = this.size; for (int i=0; modCount == expectedModCount && i < size; i++) { action.accept(elementData[i]); }
-
final Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } while (i != size && modCount == expectedModCount) { consumer.accept((E) elementData[i++]); }
-
Stream.forEach
que acabará llamandoSpliterator.forEachRemaining
if ((i = index) >= 0 && (index = hi) <= a.length) { for (; i < hi; ++i) { @SuppressWarnings("unchecked") E e = (E) a[i]; action.accept(e); } if (lst.modCount == mc) return; }
Como puede ver, el bucle interno del código de implementación, donde estos las operaciones terminan, es básicamente lo mismo, iterando sobre índices y leyendo directamente el array y pasando el elemento al Consumer
.
Cosas similares se aplican a todas las colecciones estándar del JRE, todas ellas tienen implementaciones adaptadas para todas las formas de hacerlo, incluso si está utilizando una envoltura de solo lectura. En este último caso, la API Stream
incluso ganaría ligeramente, Collection.forEach
tiene que ser llamada en la vista de solo lectura para delegar a la colección original forEach
. Del mismo modo, el iterador tiene que ser envuelto para proteger contra los intentos de invocar el método remove()
. Por el contrario, spliterator()
puede devolver directamente el Spliterator
de la colección original, ya que no tiene soporte para modificaciones. Por lo tanto, el flujo de una vista de solo lectura es exactamente el mismo que el flujo de la colección original.
Aunque todas estas diferencias apenas se notan cuando se mide el rendimiento en la vida real, como se dijo, el bucle interno, que es lo más relevante para el rendimiento, es el mismo en todos caso.
La cuestión es qué conclusión sacar de eso. Todavía puede devolver una vista de envoltura de solo lectura a la colección original, ya que el autor de la llamada todavía puede invocar stream().forEach(…)
para iterar directamente en el contexto de la colección original.
Dado que el rendimiento no es realmente diferente, debería centrarse en el diseño de nivel superior como se discute en "¿Debo devolver una colección o una transmisión?"
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:01:29