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.

Author: Miguel Gamboa, 2015-07-03

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.

  1. 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() sin hasNext(), o hasNext() varias veces sin next(), 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 el next/hasNext la dualidad es fundamentalmente picante, y las implementaciones Iterator tienen que hacer más trabajo para defenderse contra modificaciones concurrentes que las implementaciones Spliterator.)

  2. 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.

  3. Además, el recorrido a través de Spliterator tiende a tener muchas menos escrituras de montón que con Iterator. Con Iterator, cada elemento causa una o más escrituras de montón (a menos que el Iterator 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 industrial forEachRemaining 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.)

 44
Author: Brian Goetz,
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:

  • Collection.forEach

    final E[] elementData = (E[]) this.elementData;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        action.accept(elementData[i]);
    }
    
  • Iterator.forEachRemaining

    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á llamando Spliterator.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?"

 12
Author: Holger,
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