scala vs java, rendimiento y memoria?
performance memory (8)
Tengo muchas ganas de investigar Scala, y tengo una pregunta básica a la que no puedo encontrar respuesta: en general, ¿hay alguna diferencia en el rendimiento y el uso de la memoria entre Scala y Java?
El ejemplo de Java realmente no es una expresión idiomática para los programas de aplicación típicos. Tal código optimizado podría encontrarse en un método de biblioteca del sistema. Pero luego usaría una matriz del tipo correcto, es decir, File [] y no lanzaría una IndexOutOfBoundsException. (Diferentes condiciones de filtro para contar y agregar). Mi versión sería (siempre (!) Con llaves, porque no me gusta pasar una hora buscando un error que se introdujo al guardar los 2 segundos para presionar una sola tecla en Eclipse):
List<File> bigEnough = new ArrayList<File>();
for(String s : array) {
if(s.length() > 2) {
File file = mapping.get(s);
if (file != null) {
bigEnough.add(file);
}
}
}
Pero podría traerte muchos otros ejemplos feos de código Java de mi proyecto actual. Intenté evitar el estilo común de copiar y modificar la codificación al factorizar estructuras y comportamientos comunes.
En mi clase base DAO abstracta, tengo una clase interna abstracta para el mecanismo de caché común. Para cada tipo de objeto de modelo concreto hay una subclase de la clase base abstracta DAO, en la que la clase interna se subclasifica para proporcionar una implementación para el método que crea el objeto comercial cuando se carga desde la base de datos. (No podemos usar una herramienta ORM porque accedemos a otro sistema a través de una API propietaria).
Este código de subclases y creación de instancias no está nada claro en Java y sería muy legible en Scala.
Escribe tu Scala como Java, y puedes esperar que se emita un bytecode casi idéntico, con métricas casi idénticas.
Escribirlo más "idiomáticamente", con objetos inmutables y funciones de orden superior, y será un poco más lento y un poco más grande. La única excepción a esta regla empírica es cuando se utilizan objetos genéricos en los que el tipo params usa la anotación @specialised
, esto creará un bytecode aún mayor que puede superar el rendimiento de Java al evitar el boxeo / unboxing.
También vale la pena mencionar el hecho de que una mayor cantidad de memoria / menos velocidad es una desventaja inevitable al escribir código que se puede ejecutar en paralelo. El código Idiomatic Scala es mucho más declarativo en naturaleza que el código Java típico, y a menudo está a solo 4 caracteres ( .par
) de ser completamente paralelo.
Así que si
- El código de Scala tarda 1.25 veces más que el código de Java en un solo hilo
- Se puede dividir fácilmente en 4 núcleos (ahora común incluso en computadoras portátiles)
- para un tiempo de ejecución paralelo de (1.24 / 4 =) 0.3125x el Java original
¿Diría entonces que el código de Scala ahora es comparativamente un 25% más lento o 3 veces más rápido?
La respuesta correcta depende exactamente de cómo se define el "rendimiento" :)
Java y Scala compilan hasta el código byte de JVM, por lo que la diferencia no es tan grande. La mejor comparación que puede obtener es, probablemente, en el juego de parámetros de lenguaje de la computadora , que básicamente dice que Java y Scala tienen el mismo uso de memoria. Scala es solo un poco más lento que Java en algunos de los puntos de referencia enumerados, pero eso podría ser simplemente porque la implementación de los programas es diferente.
Realmente, ambos están tan cerca que no vale la pena preocuparse. El aumento de la productividad que obtienes al utilizar un lenguaje más expresivo como Scala vale mucho más que el rendimiento mínimo (si corresponde).
Juego de puntos de referencia del lenguaje de la computadora:
Prueba de velocidad java / scala 1.71 / 2.25
Prueba de memoria java / scala 66.55 / 80.81
Por lo tanto, estos puntos de referencia dicen que Java es un 24% más rápido y Scala usa un 21% más de memoria.
En general, no es gran cosa y no debería importar en aplicaciones del mundo real, donde la base de datos y la red consumen la mayor parte del tiempo.
En pocas palabras: si Scala te hace a ti y a tu equipo (y las personas que toman el proyecto cuando salgas) más productivo, entonces debes hacerlo.
Otros han respondido a esta pregunta con respecto a los bucles ajustados, aunque parece haber una diferencia de rendimiento obvia entre los ejemplos de Rex Kerr que he comentado.
Esta respuesta está realmente dirigida a personas que podrían investigar la necesidad de una optimización estricta como un defecto de diseño.
Soy relativamente nuevo en Scala (alrededor de un año más o menos), pero la sensación que tiene hasta ahora es que permite diferir muchos aspectos del diseño, la implementación y la ejecución con relativa facilidad (con suficiente experiencia de lectura y experimentación :)
Características de diseño diferido:
Funciones de implementación diferida:
Funciones de ejecución diferida: (lo siento, no hay enlaces)
- Valores perezosos a prueba de subprocesos
- Pass-by-name
- Cosas monádicas
Estas características, para mí, son las que nos ayudan a recorrer el camino hacia aplicaciones rápidas y ajustadas.
Los ejemplos de Rex Kerr difieren en qué aspectos de la ejecución se difieren. En el ejemplo de Java, la asignación de la memoria se difiere hasta que se calcule el tamaño donde el ejemplo de Scala difiere la búsqueda de la asignación. Para mí, parecen algoritmos completamente diferentes.
Esto es lo que creo que es más de manzanas a manzanas equivalentes para su ejemplo de Java:
val bigEnough = array.collect({
case k: String if k.length > 2 && mapping.contains(k) => mapping(k)
})
Sin colecciones intermedias, sin instancias de Option
, etc. Esto también conserva el tipo de colección, así que el tipo de bigEnough
es Array[File]
: la implementación de collect
Array
probablemente hará algo bigEnough
lo que hace el código de Mr Kerr en Java.
Las características de diseño diferido que enumeré anteriormente también permitirían a los desarrolladores API de colecciones de Scala implementar esa rápida implementación de recopilación específica de matrices en futuras versiones sin romper la API. Esto es a lo que me refiero con pisar el camino a la velocidad.
También:
val bigEnough = array.withFilter(_.length > 2).flatMap(mapping.get)
El método withFilter
que he usado aquí en lugar del filter
corrige el problema de la colección intermedia, pero aún existe el problema de la instancia de la opción.
Un ejemplo de velocidad de ejecución simple en Scala es con el registro.
En Java podríamos escribir algo como:
if (logger.isDebugEnabled())
logger.debug("trace");
En Scala, esto es solo:
logger.debug("trace")
porque el parámetro del mensaje para depurar en Scala tiene el tipo " => String
", que considero una función sin parámetros que se ejecuta cuando se evalúa, pero que la documentación llama pass-by-name.
EDITAR {Las funciones en Scala son objetos, por lo que hay un objeto adicional aquí. Para mi trabajo, el peso de un objeto trivial vale la pena eliminar la posibilidad de que un mensaje de registro sea evaluado innecesariamente. }
Esto no hace que el código sea más rápido, pero aumenta la probabilidad de que sea más rápido y es menos probable que tengamos la experiencia de revisar y limpiar en masa el código de otras personas.
Para mí, este es un tema constante dentro de Scala.
El código difícil no puede capturar por qué Scala es más rápido aunque insinúa un poco.
Siento que es una combinación de reutilización de código y el techo de la calidad del código en Scala.
En Java, el código increíble a menudo se ve obligado a convertirse en un desastre incomprensible y, por lo tanto, no es realmente viable dentro de las API de calidad de producción, ya que la mayoría de los programadores no podrían usarlo.
Tengo muchas esperanzas de que Scala permita que los einstein entre nosotros implementen API mucho más competentes, expresadas potencialmente a través de DSL. Las API centrales en Scala ya están muy lejos en esta ruta.
Soy un nuevo usuario, por lo que no puedo agregar un comentario a la respuesta anterior de Rex Kerr (permitiendo a los nuevos usuarios "responder" pero no "comentar" es una regla muy extraña por cierto).
Me inscribí simplemente para responder a la frase "phew, Java es tan detallado y tan difícil" insinuación de la popular respuesta de Rex anterior. Si bien, por supuesto, puede escribir un código Scala más conciso, el ejemplo de Java dado está claramente hinchado. La mayoría de los desarrolladores de Java codificarían algo como esto:
List<String> bigEnough = new ArrayList<String>();
for(String s : array) {
if(s.length() > 2 && mapping.get(s) != null) {
bigEnough.add(mapping.get(s));
}
}
Y, por supuesto, si pretendemos que Eclipse no hace la mayor parte del trabajo de tipeo real para usted y que cada personaje ahorrado realmente lo convierte en un mejor programador, entonces podría codificar esto:
List b=new ArrayList();
for(String s:array)
if(s.length()>2 && mapping.get(s) != null) b.add(mapping.get(s));
Ahora no solo ahorré el tiempo que tardé en escribir nombres completos de variables y llaves (liberándome para dedicar 5 segundos más a pensar pensamientos algorítmicos profundos), pero también puedo ingresar mi código en concursos de ofuscación y potencialmente ganar dinero extra por las vacaciones.
@higherkinded de @higherkinded sobre el tema - Consideraciones de rendimiento de Scala que hace algunas comparaciones Java / Scala.
Herramientas:
Gran blogpost:
Scala hace que sea muy fácil usar enormes cantidades de memoria sin darse cuenta. Esto suele ser muy poderoso, pero ocasionalmente puede ser molesto. Por ejemplo, supongamos que tiene una matriz de cadenas (llamada array
) y un mapa de esas cadenas a los archivos (llamado mapping
). Supongamos que desea obtener todos los archivos que están en el mapa y provienen de cadenas de longitud superior a dos. En Java, es posible que
int n = 0;
for (String s: array) {
if (s.length > 2 && mapping.containsKey(s)) n++;
}
String[] bigEnough = new String[n];
n = 0;
for (String s: array) {
if (s.length <= 2) continue;
bigEnough[n++] = map.get(s);
}
¡Uf! Trabajo duro. En Scala, la forma más compacta de hacer lo mismo es:
val bigEnough = array.filter(_.length > 2).flatMap(mapping.get)
¡Fácil! Pero, a menos que esté bastante familiarizado con el funcionamiento de las colecciones, es posible que no se dé cuenta de que esta forma de hacerlo creó una matriz intermedia adicional (con filter
) y un objeto adicional para cada elemento de la matriz (con mapping.get
, que devuelve una opción). También crea dos objetos de función (uno para el filtro y otro para el mapa plano), aunque rara vez es un problema importante ya que los objetos de función son pequeños.
Entonces, básicamente, el uso de la memoria es, en un nivel primitivo, el mismo. Pero las bibliotecas de Scala tienen muchos métodos poderosos que te permiten crear enormes cantidades de objetos (generalmente de corta duración) muy fácilmente. El recolector de basura suele ser bastante bueno con ese tipo de basura, pero si te olvidas por completo de qué memoria se está utilizando, probablemente te encuentres con problemas antes en Scala que en Java.
Tenga en cuenta que el código Scala de Computer Languages Benchmark Game está escrito en un estilo bastante similar a Java para obtener un rendimiento similar al de Java y, por lo tanto, tiene un uso de memoria similar a Java. Puede hacer esto en Scala: si escribe su código para que parezca un código Java de alto rendimiento, será un código Scala de alto rendimiento. (Puede escribirlo en un estilo de Scala más idiomático y obtener un buen rendimiento, pero depende de los detalles).
Debo agregar que por cada cantidad de tiempo dedicado a la programación, mi código Scala suele ser más rápido que mi código Java, ya que en Scala puedo hacer tediosas las partes tediosas que no son de rendimiento con menos esfuerzo y dedicar más atención a la optimización de los algoritmos y código para las partes críticas para el rendimiento.