.toArray (nueva MyClass [0]) o .toArray (nueva MyClass [myList.size ()])?


176

Suponiendo que tengo una ArrayList

ArrayList<MyClass> myList;

Y quiero llamar a Array, ¿hay alguna razón de rendimiento para usar

MyClass[] arr = myList.toArray(new MyClass[myList.size()]);

encima

MyClass[] arr = myList.toArray(new MyClass[0]);

?

Prefiero el segundo estilo, ya que es menos detallado, y asumí que el compilador se asegurará de que la matriz vacía no se cree realmente, pero me he estado preguntando si eso es cierto.

Por supuesto, en el 99% de los casos no hace la diferencia de una forma u otra, pero me gustaría mantener un estilo consistente entre mi código normal y mis bucles internos optimizados ...


66
Parece que la pregunta ahora se ha resuelto en una nueva publicación de blog de Aleksey Shipilёv, Arrays of Wisdom of the Ancients .
glts

66
De la publicación del blog: "En pocas palabras: toArray (nueva T [0]) parece más rápido, más seguro y contractualmente más limpio, y por lo tanto debería ser la opción predeterminada ahora".
DavidS

Respuestas:


109

Contraintuitivamente, la versión más rápida, en Hotspot 8, es:

MyClass[] arr = myList.toArray(new MyClass[0]);

He ejecutado un micro benchmark usando jmh, los resultados y el código están a continuación, lo que muestra que la versión con una matriz vacía supera constantemente la versión con una matriz preestablecida. Tenga en cuenta que si puede reutilizar una matriz existente del tamaño correcto, el resultado puede ser diferente.

Resultados de referencia (puntuación en microsegundos, menor = mejor):

Benchmark                      (n)  Mode  Samples    Score   Error  Units
c.a.p.SO29378922.preSize         1  avgt       30    0.025  0.001  us/op
c.a.p.SO29378922.preSize       100  avgt       30    0.155  0.004  us/op
c.a.p.SO29378922.preSize      1000  avgt       30    1.512  0.031  us/op
c.a.p.SO29378922.preSize      5000  avgt       30    6.884  0.130  us/op
c.a.p.SO29378922.preSize     10000  avgt       30   13.147  0.199  us/op
c.a.p.SO29378922.preSize    100000  avgt       30  159.977  5.292  us/op
c.a.p.SO29378922.resize          1  avgt       30    0.019  0.000  us/op
c.a.p.SO29378922.resize        100  avgt       30    0.133  0.003  us/op
c.a.p.SO29378922.resize       1000  avgt       30    1.075  0.022  us/op
c.a.p.SO29378922.resize       5000  avgt       30    5.318  0.121  us/op
c.a.p.SO29378922.resize      10000  avgt       30   10.652  0.227  us/op
c.a.p.SO29378922.resize     100000  avgt       30  139.692  8.957  us/op

Como referencia, el código:

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
public class SO29378922 {
  @Param({"1", "100", "1000", "5000", "10000", "100000"}) int n;
  private final List<Integer> list = new ArrayList<>();
  @Setup public void populateList() {
    for (int i = 0; i < n; i++) list.add(0);
  }
  @Benchmark public Integer[] preSize() {
    return list.toArray(new Integer[n]);
  }
  @Benchmark public Integer[] resize() {
    return list.toArray(new Integer[0]);
  }
}

Puede encontrar resultados similares, análisis completo y discusión en la publicación de blog Arrays of Wisdom of the Ancients . Para resumir: el compilador JVM y JIT contiene varias optimizaciones que le permiten crear e inicializar de forma económica una nueva matriz de tamaño correcto, y esas optimizaciones no se pueden usar si crea la matriz usted mismo.


2
Comentario muy interesante Me sorprende que nadie haya comentado sobre esto. Supongo que es porque contradice las otras respuestas aquí, en cuanto a velocidad. También es interesante notar que la reputación de este chico es casi más alta que todas las otras respuestas (ers) combinadas.
Pimp Trizkit

Estoy divagando. También me gustaría ver puntos de referencia para MyClass[] arr = myList.stream().toArray(MyClass[]::new);... que supongo que serían más lentos. Además, me gustaría ver puntos de referencia para la diferencia con la declaración de matriz. Como en la diferencia entre: MyClass[] arr = new MyClass[myList.size()]; arr = myList.toArray(arr);y MyClass[] arr = myList.toArray(new MyClass[myList.size()]);... ¿o no debería haber ninguna diferencia? Supongo que estos dos son un problema que está fuera de las toArrayfunciones que suceden. ¡Pero hey! No pensé que aprendería sobre las otras intrincadas diferencias.
Pimp Trizkit

1
@PimpTrizkit Recién comprobado: el uso de una variable adicional no hace la diferencia esperada, el uso de una transmisión toma entre un 60% y un 100% más de tiempo que las llamadas toArraydirectas (cuanto menor es el tamaño, mayor es la sobrecarga relativa)
asilias

¡Vaya, esa fue una respuesta rápida! ¡Gracias! Sí, sospechaba eso. La conversión a una transmisión no parecía eficiente. ¡Pero nunca se sabe!
Pimp Trizkit

2
Esta misma conclusión se encontró aquí: shipilev.net/blog/2016/arrays-wisdom-ancients
user167019

122

A partir de ArrayList en Java 5 , la matriz ya se completará si tiene el tamaño correcto (o es más grande). Por consiguiente

MyClass[] arr = myList.toArray(new MyClass[myList.size()]);

creará un objeto de matriz, lo llenará y lo devolverá a "arr". Por otra parte

MyClass[] arr = myList.toArray(new MyClass[0]);

creará dos matrices. El segundo es una matriz de MyClass con longitud 0. Por lo tanto, hay una creación de objeto para un objeto que se desechará de inmediato. En la medida en que el código fuente sugiere que el compilador / JIT no puede optimizar este, por lo que no se crea. Además, el uso del objeto de longitud cero produce fundición (es) dentro del método toArray ().

Vea la fuente de ArrayList.toArray ():

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

Use el primer método para que solo se cree un objeto y evite los moldes (implícitos pero caros).


1
Dos comentarios pueden ser de interés para alguien: 1) LinkedList.toArray (T [] a) es aún más lento (utiliza la reflexión: Array.newInstance) y más complejo; 2) Por otro lado, en el lanzamiento de JDK7, me sorprendió descubrir que Array.newInstance, que generalmente es muy lento, ¡funciona casi tan rápido como la creación de arreglos habitual!
java.is.for.desktop

1
@ktaria size es un miembro privado de ArrayList, especificando **** suprise **** el tamaño. Ver ArrayList SourceCode
MyPasswordIsLasercats

3
Adivinar el rendimiento sin puntos de referencia solo funciona en casos triviales. En realidad, new Myclass[0]es más rápido: shipilev.net/blog/2016/arrays-wisdom-ancients
Karol S

Esta respuesta ya no es válida a partir de JDK6 +
Антон Антонов

28

De la inspección de JetBrains Intellij Idea:

Hay dos estilos para convertir una colección en una matriz: ya sea usando una matriz previamente dimensionada (como c.toArray (new String [c.size ()]) ) o usando una matriz vacía (como c.toArray (new String [ 0]) .

En versiones anteriores de Java se recomendaba el uso de una matriz de tamaño predeterminado, ya que la llamada de reflexión necesaria para crear una matriz del tamaño adecuado era bastante lenta. Sin embargo, desde las últimas actualizaciones de OpenJDK 6, esta llamada fue intrinsificada, lo que hace que el rendimiento de la versión de matriz vacía sea el mismo y, a veces, incluso mejor, en comparación con la versión de tamaño predeterminado. Además, pasar una matriz previamente dimensionada es peligroso para una colección simultánea o sincronizada, ya que es posible una carrera de datos entre el tamaño y la llamada a la matriz que puede generar nulos adicionales al final de la matriz, si la colección se redujo simultáneamente durante la operación.

Esta inspección permite seguir el estilo uniforme: ya sea usando una matriz vacía (que se recomienda en Java moderno) o usando una matriz de tamaño predeterminado (que podría ser más rápida en versiones Java anteriores o JVM no basadas en HotSpot).


Si todo esto es texto copiado / citado, ¿podríamos formatearlo en consecuencia y también proporcionar un enlace a la fuente? De hecho, vine aquí por la inspección de IntelliJ y estoy muy interesado en el enlace para buscar todas sus inspecciones y el razonamiento detrás de ellas.
Tim Büthe

3
Aquí puede consultar los textos de las inspecciones: github.com/JetBrains/intellij-community/tree/master/plugins/…
Антон Антонов


17

Las JVM modernas optimizan la construcción de matriz reflectante en este caso, por lo que la diferencia de rendimiento es pequeña. Nombrar la colección dos veces en ese código repetitivo no es una gran idea, por lo que evitaría el primer método. Otra ventaja de la segunda es que funciona con colecciones sincronizadas y concurrentes. Si desea optimizar, reutilice la matriz vacía (las matrices vacías son inmutables y se pueden compartir), o use un generador de perfiles (!).


2
La votación positiva 'reutiliza la matriz vacía', porque es un compromiso entre la legibilidad y el rendimiento potencial que es digno de consideración. Pasar un argumento declarado private static final MyClass[] EMPTY_MY_CLASS_ARRAY = new MyClass[0]no evita que la matriz devuelta se construya por reflexión, pero evita que se construya una matriz adicional cada vez.
Michael Scheper

Machael tiene razón, si usa una matriz de longitud cero no hay forma de evitar: (T []) java.lang.reflect.Array.newInstance (a.getClass (). GetComponentType (), size); lo que sería superfluo si el tamaño fuera> = actualSize (JDK7)
Alex

Si puede dar una cita para "las JVM modernas optimizan la construcción de matrices reflectantes en este caso", con gusto votaré esta respuesta.
Tom Panning

Estoy aprendiendo aqui Si en cambio uso: MyClass[] arr = myList.stream().toArray(MyClass[]::new);¿Ayudaría o perjudicaría con colecciones sincronizadas y concurrentes? ¿Y por qué? Por favor.
Pimp Trizkit

3

toArray verifica que la matriz pasada sea del tamaño correcto (es decir, lo suficientemente grande como para ajustarse a los elementos de su lista) y, de ser así, la usa. Por consiguiente, si el tamaño de la matriz lo proporciona más pequeño de lo requerido, se creará una nueva matriz por reflejo.

En su caso, una matriz de tamaño cero es inmutable, por lo que podría elevarse con seguridad a una variable final estática, lo que podría hacer que su código sea un poco más limpio, lo que evita crear la matriz en cada invocación. De todos modos, se creará una nueva matriz dentro del método, por lo que es una optimización de legibilidad.

Podría decirse que la versión más rápida es pasar la matriz de un tamaño correcto, pero a menos que pueda probar que este código es un cuello de botella de rendimiento, prefiera la legibilidad al rendimiento en tiempo de ejecución hasta que se demuestre lo contrario.


2

El primer caso es más eficiente.

Eso es porque en el segundo caso:

MyClass[] arr = myList.toArray(new MyClass[0]);

el tiempo de ejecución realmente crea una matriz vacía (con tamaño cero) y luego dentro del método toArray crea otra matriz para adaptarse a los datos reales. Esta creación se realiza utilizando la reflexión utilizando el siguiente código (tomado de jdk1.5.0_10):

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        a = (T[])java.lang.reflect.Array.
    newInstance(a.getClass().getComponentType(), size);
System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

Al usar la primera forma, evita la creación de una segunda matriz y también evita el código de reflexión.


toArray () no usa la reflexión. Al menos, siempre y cuando no cuentes el "casting" a la reflexión, de todos modos ;-).
Georgi

toArray (T []) lo hace. Necesita crear una matriz del tipo apropiado. Las JVM modernas optimizan ese tipo de reflexión para que tenga la misma velocidad que la versión no reflectante.
Tom Hawtin - tackline

Creo que sí usa la reflexión. El JDK 1.5.0_10 lo hace con seguridad y la reflexión es la única forma en que sé crear una matriz de un tipo que no conoces en el momento de la compilación.
Panagiotis Korros

Entonces, uno de los ejemplos de código fuente ella (el anterior o el mío) está desactualizado. Sin embargo, lamentablemente no encontré un número de subversión correcto para el mío.
Georgi

1
Georgi, tu código es de JDK 1.6 y si ves la implementación del método Arrays.copyTo verás que la implementación usa reflexión.
Panagiotis Korros

-1

El segundo es marginalmente más legible, pero hay tan poca mejora que no vale la pena. El primer método es más rápido, sin desventajas en tiempo de ejecución, así que eso es lo que uso. Pero lo escribo de la segunda manera, porque es más rápido escribir. Entonces mi IDE lo marca como una advertencia y ofrece arreglarlo. Con una sola pulsación de tecla, convierte el código del segundo tipo al primero.


-2

El uso de 'toArray' con la matriz del tamaño correcto funcionará mejor ya que la alternativa creará primero la matriz de tamaño cero y luego la matriz del tamaño correcto. Sin embargo, como usted dice, es probable que la diferencia sea insignificante.

Además, tenga en cuenta que el compilador javac no realiza ninguna optimización. En la actualidad, los compiladores JIT / HotSpot realizan todas las optimizaciones en tiempo de ejecución. No conozco ninguna optimización en torno a 'toArray' en ninguna JVM.

La respuesta a su pregunta, entonces, es en gran medida una cuestión de estilo, pero por razones de coherencia debe formar parte de cualquier estándar de codificación al que se adhiera (ya sea documentado o no).


OTOH, si el estándar es usar una matriz de longitud cero, los casos que se desvían implican que el rendimiento es una preocupación.
Michael Scheper

-5

código de muestra para entero:

Integer[] arr = myList.toArray(new integer[0]);
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.