Penalización de rendimiento de String.intern ()

Muchas personas hablan sobre las ventajas de rendimiento de String.intern (), pero en realidad estoy más interesado en cuál puede ser la penalización de rendimiento.

Mis principales preocupaciones son:

  • Costo de búsqueda : el tiempo que toma el interno () para determinar si la cadena interna existe en el grupo de constantes. ¿Cómo escala ese costo la escala con el número de cadenas en ese grupo?
  • Sincronización : obviamente, el conjunto constante es compartido por toda la JVM. ¿Cómo se comporta ese grupo cuando se llama una y otra vez a intern () desde varios subprocesos? ¿Cuánto locking realiza? ¿Cómo se escala el rendimiento con contención?

Me preocupan todas estas cosas porque actualmente estoy trabajando en una aplicación financiera que tiene el problema de usar demasiada memoria debido a las cadenas duplicadas. Algunas cadenas parecen básicamente valores enumerados y solo pueden tener un número limitado de valores potenciales (como los nombres de moneda (“USD”, “EUR”)) en más de un millón de copias. String.intern () parece una obviedad en este caso, pero estoy preocupado por la sobrecarga de sincronización de llamar a intern () cada vez que almaceno una moneda en alguna parte.

Además de eso, algunos otros tipos de cadenas pueden tener millones de valores diferentes, pero todavía tienen decenas de miles de copias de cada uno (como los códigos ISIN). Para estos, me preocupa que el internar un millón de cadenas ralentizaría básicamente el método interno () tanto como para empantanar mi aplicación.

Hice un poco de benchmarking yo mismo. Para la parte del costo de búsqueda, he decidido comparar String.intern () con ConcurrentHashMap.putIfAbsent (s, s). Básicamente, esos dos métodos hacen las mismas cosas, excepto que String.intern () es un método nativo que almacena y lee desde un SymbolTable que se administra directamente en la JVM, y ConcurrentHashMap.putIfAbsent () es solo un método de instancia normal.

Puede encontrar el código de referencia en github gist (por falta de un lugar mejor para ponerlo). También puede encontrar las opciones que utilicé al iniciar la JVM (para verificar que la referencia no esté sesgada) en los comentarios en la parte superior del archivo fuente.

De todos modos aquí están los resultados:

Costo de búsqueda (rosca simple)

Leyenda

  • recuento : el número de cadenas distintas que estamos tratando de agrupar
  • interno inicial : el tiempo en ms que se tardó en insertar todas las cadenas en el grupo de cadenas
  • buscar la misma cadena : el tiempo en ms que se tardó en buscar cada una de las cadenas nuevamente desde el grupo, usando exactamente la misma instancia que se ingresó previamente en el grupo
  • cadena de búsqueda igual : el tiempo en ms que tardó en buscar cada una de las cadenas desde el grupo, pero utilizando una instancia diferente

String.intern ()

count initial intern lookup same string lookup equal string 1'000'000 40206 34698 35000 400'000 5198 4481 4477 200'000 955 828 803 100'000 234 215 220 80'000 110 94 99 40'000 52 30 32 20'000 20 10 13 10'000 7 5 7 

ConcurrentHashMap.putIfAbsent ()

 count initial intern lookup same string lookup equal string 1'000'000 411 246 309 800'000 352 194 229 400'000 162 95 114 200'000 78 50 55 100'000 41 28 28 80'000 31 23 22 40'000 20 14 16 20'000 12 6 7 10'000 9 5 3 

La conclusión del costo de búsqueda: String.intern () es sorprendentemente caro de llamar. Se escala extremadamente mal, en algo de O (n) donde n es el número de cadenas en el conjunto. Cuando crece el número de cadenas en el conjunto, la cantidad de tiempo para buscar una cadena del grupo crece mucho más (0,7 microsegundos por búsqueda con 10,000 cadenas, 40 microsegundos por búsqueda con 1 000000 cadenas).

ConcurrentHashMap escala como se espera, el número de cadenas en el conjunto no tiene impacto en la velocidad de la búsqueda.

Basado en este experimento, le sugiero que evite usar String.intern () si va a internar más de unas cuantas cuerdas.

Recientemente escribí un artículo sobre la implementación de String.intern () en Java 6, 7 y 8: String.intern en Java 6, 7 y 8 – agrupación de cadenas .

Existe un parámetro -XX: StringTableSize JVM, que le permitirá hacer que String.intern sea extremadamente útil en Java7 +. Por lo tanto, desafortunadamente tengo que decir que esta pregunta actualmente está dando información engañosa a los lectores.

He encontrado que es mejor usar una tabla de hash fastutil y hacer mi propia práctica en lugar de volver a utilizar String.intern() . Usar mi propia tabla hash significa que puedo tomar mis propias decisiones sobre la concurrencia, y no estoy compitiendo por el espacio PermGen.

Hice esto porque estaba trabajando en un problema que tenía, por así decirlo, millones de cadenas, muchas idénticas, y quería (a) reducir la huella y (b) permitir la comparación por identidad. Para mi problema, las cosas fueron mejores con la internación que sin ella, utilizando mi String.intern() no String.intern() .

YMMV.

El siguiente micro benchmark sugiere usar una enumeración que ofrece una mejora de rendimiento de alrededor de diez veces (se aplican las advertencias de micro benchmark habituales) de la siguiente manera:

 public class Test { private enum E { E1; private static final Map named = new HashMap(); static { for (E e : E.values()) { named.put( e.name(), e ); } } private static E get(String s) { return named.get( s ); } } public static void main(String... strings) { E e = E.get( "E1" ); // ensure map is initialised long start = System.nanoTime(); testMap( 10000000 ); long end = System.nanoTime(); System.out.println( 1E-9 * (end - start) ); } private static void testIntern(int num) { for (int i = 0; i < num; i++) { String s = "E1".intern(); } } private static void testMap(int num) { for (int i = 0; i < num; i++) { E e = E.get( "E1" ); } } } 

Resultados (10 millones de iteraciones): testIntern () - 0.8 seconds testMap () - 0.06 segundos

Por supuesto, YMMV, pero las enumeraciones ofrecen tantos beneficios sobre las cadenas ... la seguridad de tipo sobre otras cadenas aleatorias, la capacidad de agregar métodos, etc. parece ser la mejor manera de hacerlo.