Strings compactos

0
2476

Java 6 introdujo un mecanismo para almacenar caracteres ASCII en byte[] en lugar de en char[]. Esta característica se eliminó de nuevo en Java 7. Aún así, volverá en Java 9, pero en esta ocasión, por defecto está habilitada la compresión y se usa siempre byte[].

[box style=”1″]

Éste artículo es una traducción al castellano de la entrada original publicada, en inglés, por Dr. Heinz Kabutz en su número 237 del JavaSpecialists newsletter. Puedes consultar el texto original en Javaspecialists’ Newsletter #237: String compaction

Este artículo se publica en Adictos al Trabajo, con permiso del autor, traducido por David Gómez García, (@dgomezg) consultor tecnológico en Autentia, colaborador de Javaspecialists e instructor certificado para impartir los cursos de Javaspecialists en Español.
[/box]

Strings compactos

Estuve una semana entera instruyendo a un grupo de buenos desarrolladores Java de Provinzial Rheinland Versicherung AG sobre las sutilezas de los patrones de diseño en Java. Os sorprendería saber que mi Patterns Course es, de todos, el más popular. Ni el Advanced concurrency and performance for Java 8 (aunque también está bastante solicitado), ni el curso de introdución a Java (no he impartido ninguno en los últimos años), ni siquiera el Advanced Topics in Java. No, mi humilde curso de patrones de diseño que escribí ya en 2001 es todavía el más demandando en la actualidad.

Normalmente, cuando imparto mi curso de patrones, vemos todo tipo de temas relacionados en Java. De esta forma, los asistentes aprenden mucho más de lo que encontrarían en cualquier libro: pueden ver dónde se utilizan los patrones en la propia JDK; aprenden buenos principios de diseño; conocen las últimas mejoras de Java 8, aunque aún estén anclados en JDK 6 o 7; incluso tocamos un poquito de concurrencia. Este es el curso que, una vez que una empresa ha inscrito a sus programadores, habitualmente siguen enviando a más y más de ellos. Esta es la razón por la que, 15 años después de escribir la primera versión, aún es popular entre las empresas.

En una de esas jornadas, estábamos revisando el patrón Flyweight, que tiene una estructura de clases bastante extraña. No es realmente un patrón de diseño. Sin embargo, como el Facade, es un mal necesario en los patrones de diseño. Me explico. Un buen diseño orientado a objetos produce sistemas altamente configurables y reducen la duplicidad de código. Y esto es bueno, pero también implica a veces bastante trabajo para utilizar el sistema. El patrón Facade hace que un subsistema complejo sea más fácil de utilizar. ¿Por qué es complejo?. Porque normalmente tenemos muchas formas de usarlo, gracias a un uso generoso de los propios patrones de diseño. Flyweight tiene una razón de ser parecida. Normalmente, los buenos diseños en orientación a objetos tienen muchos más objetos (e instancias) que los diseños monolíticos, donde todo es un Singleton. Flyweight trata de reducir el número de objetos, compartiendo las instancias de aquellos que son iguales; lo que es posible si desde fuera no dependemos (o modificamos) el estado interno de los mismos.

Estábamos viendo la fusión de Strings en clase, y cómo el char[] interno en String se reemplaza con un char[] compartido cuando tenemos varios String que contienen el mismo valor. Para ello hay que utilizar G1 como Garbage Collector (-XX:+UseG1GC) y también activar la característica de String deduplication (-XX:+UseStringDeduplication). Funciona muy bien en Java 8. Pero quería comprobar si estaba activo por defecto en Java 9, dado que G1 es su Garbage Collector por defecto. Me sorprendió que mi código ahora produjese un ClassCastException al intentar convertir el campo value de String a char[].

En algún momento con Java 6, aparecieron los String comprimidos. Dicho comportamiento estaba desactivado por defecto, aunque se podía activar con -XX:+UseCompressedStrings. Al activarlo, los Strings que contienen únicamente caracteres ASCII (codificables en 7-bits) pasan automáticamente a tener un byte[] como estructura interna. Con sólo un carácter en el String que necesite más de 7 bits para codificarlo, el String utilizaba de nuevo un char[]. Más curioso es cuando el String contiene caracteres UTF-16, como los del alfabeto Hindi Devanagari, porque en ese caso se crean objetos adicionales y al final tenemos una tasa de creación de objetos aún mayor que sin la compresión de Strings. Pero para el caso de los caracteres US ASCII, es perfecto. Por alguna razón, esta característica de Java 6 se descartó en Java 7 y la opción para activarlo se eliminó definitivamente en Java 8.

Pero con Java 9, aparece la nueva opción -XX:+CompactStrings, activa por defecto. Si examinas internamente la clase String, verás que siempre almacena los caracteres de la cadena en un byte[]. También hay un nuevo campo de tipo byte que almacena el encoding, que de momento puede ser Latin1 (0) o UTF-16 (1). En el futuro podrían añadirse otros valores. Así que, si tus caracteres son Latin1, tu String ocupará menos memoria.

Para probarlo, he escrito un pequeño programa en Java que puede ejecutarse con Java 6, Java 7 y Java 9 para ver las diferencias:

Esta es la primera ejecución, con Java 6 y el flag -XX:-UseCompressedStrings (por defecto). Observa como cada uno de los Strings contiene un char[].

La segunda vez, lo ejecutamos con Java 6 y -XX:+UseCompressedStrings. La cadena “hello world” contiene un byte[] y las otras dos un char[]. Sólo se comprimen los caracteres US ASCII (de 7 bits).

En Java 7 el flag se ignora. En Java 8 se eliminó, por tanto, la JVM con -XX:+UseCompressedStrings no arrancará. Por supuesto, todos los Strings contienen un char[].

En Java 9 tenemos el nuevo flag -XX:+CompactStrings, activo por defecto. Los Strings ahora almacenan siempre su carga en un byte[], independientenmente del encoding. Por ejemplo, vemos que para Latin1, todos los bytes están comprimidos.

Naturalmente, podemos desactivar esta característica de Java 9 con -XX:-CompactStrings. Sin embargo, la estructura de String ha cambiado así que, independientemente de lo que hagas, el campo value sigue siendo un byte[].

Cualquiera que utilice introspección para acceder a las tripas de String, podría obtener ahora un ClassCastException. Esperemos que la cantidad de esos programadores sea infinitamente pequeño.

Más preocupante es el rendimiento. Métodos como String.charAt(int) solían ser rápidos como la pólvora. Puedo notar una ralentización en Java 9. Si recorres habitualmente las cadenas con charAt(), será mejor que evalues algunas alternativas, ¡aunque estoy seguro de cuáles!. O quizás lo arreglen en la release final de Java 9, al fin y al cabo estoy trabajando con una Early Release (EA).

Me contaron un truquito de Peter Lawrey en una de las JCrete Unconferences: String tiene un constructor que recibe como parámetros un char[] y un boolean. El parámetro boolean no se utiliza nunca y se supone que debes pasarlo como true, indicando que el char[] se utilizará directamente como value y no se copiará. Este es el código:

El truco de Lawrey consiste en crear Strings de forma muy rápida desde un char[] utilizando directamente este constructor. No estoy seguro de los detalles, pero lo más probable es que utilice JavaLangAccess que nos proporciona la clase SharedSecrets. Antes de Java 9, esta clase estaba en el paquete sun.misc.package. Desde Java 9, está en jdk.internal.misc. Espero que no estés utilizando éste constructor directamente, porque tendrás que cambiar tu código cuando actualices a Java 9.

Aquí tienes el código. Tendrás que cambiar los imports en función de la versión de Java que uses

En resumen, si hablas inglés, alemán, francés o español, tus Strings son ahora mucho más ligeros. Para los griegos y los chinos, siguen siendo lo mismo. Para todos, seguramente resultarán un poco más lentos.

Saludos desde el aeropuerto de Tesalónica.

Heinz.

Dejar respuesta

Please enter your comment!
Please enter your name here