CLR String Interning

by Valeriano Tortola 4. octubre 2007 22:29

Un String, es un tipo de referencia especial llamado inmutable, que quiere decir que el dato almacenado en el heap no se puede cambiar, cuando asignamos un nuevo valor a una variable de este tipo, un nuevo dato es generado en otra posición del heap y su dirección de memoria asignada a dicha variable, el dato anterior queda listo para ser GarbageCollected. Bien, pues este comportamiento que ... a unos les gusta más... a otros menos... se ideó así para evitar condiciones de anticipación en entornos multithreading tanto por motivos de consistencia como de seguridad, y tiene la evidente desventaja del engorro de memoria y trabajo extra para el GC que provoca.

El String Interning es una técnica de optimización que aplica el CLR sobre los String, que consiste en que los literales de este tipo de nuestro dominio de aplicación, son almacenados en un HashTable interno, de forma que si dos variables tienen el mismo valor y este ha sido internado, ambas variables apuntan a la misma referencia. Esto es facilmente comprobable con este simple código:

String s1 = "Darker than BLACK";
String s2 = "Darker than BLACK";
 
Console.WriteLine(Object.ReferenceEquals(s1, s2)); // True

Ó por ejemplo:

String s1 = "Darker than BLACK";
 
Console.WriteLine(Object.ReferenceEquals(s1, "Darker than BLACK")); // True

Este proceso de internamiento se lleva a cabo por el JIT de forma dinámica, y como decía, solo sobre los literales, si el String es el resultado de una operación no es internado:

String s1 = "Darker than BLACK";
StringBuilder sb1 = new StringBuilder("Darker than BLACK");
 
Console.WriteLine(Object.ReferenceEquals(s1, sb1.ToString())); // False

Igual pasa con el resto de operaciones...

String s1 = "Darker than BLACK";
 
Console.WriteLine(Object.ReferenceEquals(s1, "Darker than"+" BLACK")); // True

Oh Wait! Que sucede aquí? Pues que el CLR como siempre, optimizando al máximo, entiende esa concatenación de literales como un literal tal cual porque inevitablemente va a resultar en dicho dato jejeje, ... un poco más dificil:

String s1 = "Darker than BLACK";
String s2 = String.Empty;
foreach (char c in s1) s2 += c;
 
Console.WriteLine(Object.ReferenceEquals(s1, s2)); // False

Podemos insertar directamente un String en la tabla de internados mediante el método String.Intern que nos devolverá un String apuntando al dato internado, si no existiese lo crea:

String s1 = "Darker than BLACK";
String s2 = String.Empty;
foreach (char c in s1) s2 += c;
 
Console.WriteLine(Object.ReferenceEquals(s1, s2)); // False
 
s2 = String.Intern(s2);
 
Console.WriteLine(Object.ReferenceEquals(s1, s2)); // True

Cabe destacar, que los String que añadamos a dicha tabla, no pueden ser recolectados por el GC hasta que se descargue el AppDomain, con lo que indica que habrá objetos inmortales en el heap hasta entonces ocupando memoria.

Además, añadir elementos a la tabla de internados también tiene su coste en rendimiento, pero un uso intencionado de esta característica puede mejorar mucho el rendimiento si se aprovecha bien, ya que podriamos realizar comparaciones de Strings con Object.ReferenceEquals en lugar de String.Equals(==), ya que el primero es más rápido al simplemente comparar las referencias, mientras que el segundo primero para saberlo evalua el número de caracteres y si coincide también caracter a caracter, pero claro, esto en el caso de que trabajemos siempre con los mismos String y esten todos internados :P

También podemos simplemente consultar si un String ha sido internado con el método String.IsInterned, que devuelve el String internado en caso de que sí, y nulo en caso de que no:

StringBuilder sb1 = new StringBuilder();
sb1.AppendFormat("{0} is {1}", "Hei", "BK201");
 
Console.WriteLine(String.IsInterned(sb1.ToString())==null); // True
Console.WriteLine(String.IsInterned("BK201") == null); // False

En el CLR 2.0, este comportamiento es por defecto, y aunque existe el atributo CompilationRelaxations que permitiria anularlo ... el CLR se caga en él lo ignora. En cualqueir caso no se debe desarrollar dando por hecho este comportamiento ya que en futuras versiones del CLR podría cambiar.

Ventaja? Desventaja? Verdaderamente es algo engorroso. Por un lado se suele recomendar no trabajar nunca con String harcodeados (literales) en nuestro código, y por otro siempre que se trabaja con Strings se suele realizar operaciones con ellas por lo que es dificil asegurar que un String a evaluar este internado. Aunque es cierto que como comentaba antes podamos sacar partido a esta funcionalidad en algunas ocasiones contadas, por ejemplo si obtenemos de una BD una serie de cadenas de texto que no van a ser alteradas durante la ejecución y solo se utilizan como referencia de comparación ... podemos sacar partido a Object.ReferenceEquals para su evaluación.

Y después de este largo post, que no tiene más utilidad que ser una curiosidad insana del CLR, aún te quieres devanar los sesos un poco más ... realmente ... ¿el método String.IsInterned funciona bien?

String s1 = "Black Shinigami";
StringBuilder sb = new StringBuilder("Black Shinigami");
String s2 = sb.ToString();
 
Console.WriteLine(String.IsInterned(s1) != null); // True
Console.WriteLine(String.IsInterned(s2) != null); // True
Console.WriteLine(Object.ReferenceEquals(s1, s2)); // False

Según este código, el CLR entiende que s1 y s2 estan internados, pero realmente solo lo esta s1, aunque s2 contiene un dato internado ... la posición del heap a la que apunta dicha variable no es la que esta internada ... ¿que sucede? Pues ya que en los foros del MSDN no saben no entienden mi inglés xD ... la solución me la ha dado el fabuloso libro CLR via C#, donde se explica que el HashTable de las String internadas, usa el dato como key y la dirección de memoria del dato internado como valor, lo que explica que el CLR busca el dato contenido en s2 como clave en el HashTable y obtiene la dirección de memoria en el heap de s1, que es la que devuelve exactamente ... sabiendo esto podemos usar un método mejor:

static Boolean IsThisInterned(String str)
{
  return Object.ReferenceEquals(String.IsInterned(str), str);
}

Ya finalizando, aclarar que el String Pooling es una técnica similar pero distinta, que aplica el compilador a las Strings cuando genera los metadatos, de forma que si hay varias Strings literales con el mismo dato en el código fuetne, se introduce una sola vez en los metadatos y todas apuntan a esta. De esta forma se reduce el tamaño final del ensamblado. Esto es algo que ya hacían de forma parecida los compiladores de C/C++.

Como nota final diré que si, que me encanta de Darker Than BLACK :D, de hecho me estoy descargando ahora mismo el último episodio, algo triste pero impacientemente esperado a la vez.

Hasta otro capítulo de cosas que valen para poco.

Tags: , ,

.NET 2.0 | C# 2.0

Comentarios

04/10/2007 23:32:38 #

trackback

Trackback from vtortola

CLR String Interning

vtortola |

06/10/2007 19:14:35 #

David

frikilevel+=1000;

Enhorabuena jajajaja! Pedazo de post paa luego decir cosas que valen para poco :-D

David España |

04/03/2008 22:11:47 #

pingback

Pingback from dvilchez.net

dvilchez.net  » Blog Archive   » Concurso de Plain Concepts

dvilchez.net |

Comentarios no permitidos