¿La conversión de Java introduce gastos generales? ¿Por qué?

104

¿Hay alguna sobrecarga cuando proyectamos objetos de un tipo a otro? ¿O el compilador simplemente resuelve todo y no hay ningún costo en tiempo de ejecución?

¿Es esto algo general o hay casos diferentes?

Por ejemplo, supongamos que tenemos una matriz de Object [], donde cada elemento puede tener un tipo diferente. Pero siempre sabemos con certeza que, digamos, el elemento 0 es un Double, el elemento 1 es una Cadena. (Sé que este es un diseño incorrecto, pero supongamos que tuve que hacer esto).

¿La información de tipo de Java todavía se mantiene en tiempo de ejecución? O todo se olvida después de la compilación, y si hacemos elementos (Double) [0], simplemente seguiremos el puntero e interpretaremos esos 8 bytes como un doble, ¿lo que sea?

No tengo muy claro cómo se hacen los tipos en Java. Si tiene alguna recomendación sobre libros o artículos, gracias también.

Phil
fuente
El rendimiento de instanceof y casting es bastante bueno. Publiqué algo de tiempo en Java7 sobre diferentes enfoques del problema aquí: stackoverflow.com/questions/16320014/…
Wheezil
Esta otra pregunta tiene muy buenas respuestas stackoverflow.com/questions/16741323/…
user454322

Respuestas:

78

Hay 2 tipos de casting:

Conversión implícita , cuando se transmite de un tipo a un tipo más amplio, que se realiza automáticamente y no hay gastos generales:

String s = "Cast";
Object o = s; // implicit casting

Casting explícito , cuando pasas de un tipo más amplio a uno más estrecho. Para este caso, debe usar explícitamente la transmisión de esa manera:

Object o = someObject;
String s = (String) o; // explicit casting

En este segundo caso, hay una sobrecarga en tiempo de ejecución, porque los dos tipos deben verificarse y, en caso de que la conversión no sea factible, JVM debe lanzar una ClassCastException.

Tomado de JavaWorld: The cost of casting

La fundición se utiliza para convertir entre tipos, entre tipos de referencia en particular, para el tipo de operación de fundición en la que estamos interesados ​​aquí.

Las operaciones ascendentes (también llamadas conversiones de ampliación en la especificación del lenguaje Java) convierten una referencia de subclase en una referencia de clase antecesora. Esta operación de conversión es normalmente automática, ya que siempre es segura y puede ser implementada directamente por el compilador.

Las operaciones Downcast (también llamadas conversiones de restricción en la Especificación del lenguaje Java) convierten una referencia de clase antecesora en una referencia de subclase. Esta operación de conversión crea una sobrecarga de ejecución, ya que Java requiere que la conversión se verifique en tiempo de ejecución para asegurarse de que sea válida. Si el objeto al que se hace referencia no es una instancia del tipo de destino para la conversión o una subclase de ese tipo, el intento de conversión no está permitido y debe generar una excepción java.lang.ClassCastException.

Alex Ntousias
fuente
101
Ese artículo de JavaWorld tiene más de 10 años, así que tomaría cualquier declaración que haga sobre el rendimiento con un gran grano de su mejor sal.
skaffman
1
@skaffman, de hecho, tomaría cualquier declaración que haga (independientemente del rendimiento o no) con un grano de sal.
Pacerier
¿Será el mismo caso, si no asigno el objeto fundido a la referencia y simplemente llamo al método en él? ((String)o).someMethodOfCastedClass()
Me
3
Ahora el artículo tiene casi 20 años. Y las respuestas también tienen muchos años. Esta pregunta necesita una respuesta moderna.
Raslanove
¿Qué hay de los tipos primitivos? Quiero decir, por ejemplo, ¿la conversión de int a short causa una sobrecarga similar?
luke1985
44

Para una implementación razonable de Java:

Cada objeto tiene un encabezado que contiene, entre otras cosas, un puntero al tipo de ejecución (por ejemplo , Doubleo String, pero nunca podría ser CharSequenceo AbstractList). Suponiendo que el compilador en tiempo de ejecución (generalmente HotSpot en el caso de Sun) no puede determinar el tipo estáticamente, el código de máquina generado debe realizar algunas comprobaciones.

Primero se debe leer ese puntero al tipo de tiempo de ejecución. Esto es necesario para llamar a un método virtual en una situación similar de todos modos.

Para convertir a un tipo de clase, se sabe exactamente cuántas superclases hay hasta que acierta java.lang.Object, por lo que el tipo se puede leer con un desplazamiento constante desde el puntero de tipo (en realidad, los primeros ocho en HotSpot). Nuevamente, esto es análogo a leer un puntero de método para un método virtual.

Entonces, el valor leído solo necesita una comparación con el tipo estático esperado del elenco. Dependiendo de la arquitectura del conjunto de instrucciones, otra instrucción necesitará bifurcarse (o fallar) en una bifurcación incorrecta. Los ISA como ARM de 32 bits tienen instrucción condicional y pueden hacer que el camino triste pase por el camino feliz.

Las interfaces son más difíciles debido a la herencia múltiple de interfaces. Generalmente, las dos últimas conversiones a interfaces se almacenan en caché en el tipo de tiempo de ejecución. En los primeros días (hace más de una década), las interfaces eran un poco lentas, pero eso ya no es relevante.

Con suerte, puede ver que este tipo de cosas son en gran medida irrelevantes para el rendimiento. Tu código fuente es más importante. En términos de rendimiento, el mayor impacto en su escenario es probable que sean errores de caché al perseguir punteros de objetos por todas partes (la información de tipo, por supuesto, será común).

Tom Hawtin - tackline
fuente
1
interesante - entonces esto significa que para las clases que no son de interfaz si escribo Superclass sc = (Superclass) subclass; que el compilador (jit ie: load time) colocará "estáticamente" el desplazamiento de Object en cada una de las Superclase y Subclase en sus encabezados de "Clase" y luego mediante un simple agregar + comparar podrá resolver las cosas? - eso es agradable y rápido :) Para las interfaces, supongo que no es peor que una pequeña tabla hash o btree.
peterk
@peterk Para la conversión entre clases, tanto la dirección del objeto como el "vtbl" (tabla de punteros de método, más tabla de jerarquía de clases, caché de interfaz, etc.) no cambian. Entonces, el elenco de [clase] verifica el tipo, y si encaja, no tiene que suceder nada más.
Tom Hawtin - tackline
8

Por ejemplo, supongamos que tenemos una matriz de Object [], donde cada elemento puede tener un tipo diferente. Pero siempre sabemos con certeza que, digamos, el elemento 0 es un Double, el elemento 1 es una Cadena. (Sé que este es un diseño incorrecto, pero supongamos que tuve que hacer esto).

El compilador no toma nota de los tipos de elementos individuales de una matriz. Simplemente verifica que el tipo de expresión de cada elemento sea asignable al tipo de elemento de matriz.

¿La información de tipo de Java todavía se mantiene en tiempo de ejecución? O todo se olvida después de la compilación, y si hacemos elementos (Double) [0], simplemente seguiremos el puntero e interpretaremos esos 8 bytes como un doble, ¿lo que sea?

Alguna información se mantiene en tiempo de ejecución, pero no los tipos estáticos de los elementos individuales. Puede saberlo mirando el formato de archivo de la clase.

Teóricamente es posible que el compilador JIT pueda usar "análisis de escape" para eliminar verificaciones de tipo innecesarias en algunas asignaciones. Sin embargo, hacer esto en la medida que sugiere estaría más allá de los límites de la optimización realista. La recompensa de analizar los tipos de elementos individuales sería demasiado pequeña.

Además, la gente no debería escribir código de aplicación como ese de todos modos.

Stephen C
fuente
1
¿Qué pasa con los primitivos? (float) Math.toDegrees(theta)¿Habrá aquí también una sobrecarga significativa?
SD
2
Hay una sobrecarga para algunos moldes primitivos. Si es significativo depende del contexto.
Stephen C
6

Se llama a la instrucción de código de bytes para realizar la conversión en tiempo de ejecución checkcast. Puede desensamblar el código Java utilizando javappara ver qué instrucciones se generan.

Para las matrices, Java mantiene la información de tipos en tiempo de ejecución. La mayoría de las veces, el compilador detectará errores de tipo por usted, pero hay casos en los que se encontrará con un ArrayStoreExceptioncuando intente almacenar un objeto en una matriz, pero el tipo no coincide (y el compilador no lo detectó) . La especificación del lenguaje Java da el siguiente ejemplo:

class Point { int x, y; }
class ColoredPoint extends Point { int color; }
class Test {
    public static void main(String[] args) {
        ColoredPoint[] cpa = new ColoredPoint[10];
        Point[] pa = cpa;
        System.out.println(pa[1] == null);
        try {
            pa[0] = new Point();
        } catch (ArrayStoreException e) {
            System.out.println(e);
        }
    }
}

Point[] pa = cpaes válido ya que ColoredPointes una subclase de Point, pero pa[0] = new Point()no es válido.

Esto se opone a los tipos genéricos, donde no hay información de tipo guardada en tiempo de ejecución. El compilador inserta checkcastinstrucciones donde sea necesario.

Esta diferencia en la escritura de tipos y matrices genéricos hace que a menudo no sea adecuado mezclar matrices y tipos genéricos.

JesperE
fuente
2

En teoría, se introducen gastos generales. Sin embargo, las JVM modernas son inteligentes. Cada implementación es diferente, pero no es descabellado asumir que podría existir una implementación que JIT optimizó las comprobaciones de descarte cuando podría garantizar que nunca habría un conflicto. En cuanto a qué JVM específicas ofrecen esto, no podría decírselo. Debo admitir que me gustaría conocer los detalles de la optimización JIT, pero estos son para que los ingenieros de JVM se preocupen.

La moraleja de la historia es escribir primero un código comprensible. Si experimenta ralentizaciones, perfile e identifique su problema. Hay muchas probabilidades de que no se deba al casting. Nunca sacrifique un código limpio y seguro en un intento de optimizarlo HASTA QUE SEPA QUE LO NECESITA.

HesNotTheStig
fuente