Si las cadenas son inmutables in.NET, entonces, ¿por qué la subcadena toma O (n) tiempo?


Dado que las cadenas son inmutables en. NET, me pregunto por qué han sido diseñadas de tal manera que string.Substring() toma O(substring.Length) tiempo, en lugar de O(1)?

Es decir, ¿cuáles fueron las compensaciones, si las hubo?

Author: Soner Gönül, 2011-07-19

5 answers

ACTUALIZACIÓN: Me gustó mucho esta pregunta, acabo de bloguearla. Ver Cadenas, inmutabilidad y persistencia


La respuesta corta es: O(n) es O(1) si n no crecen. La mayoría de la gente extrae subcadenas diminutas de cadenas diminutas, por lo que la forma en que la complejidad crece asintóticamente es completamente irrelevante.

La respuesta larga es:

Una estructura de datos inmutable construida de tal manera que las operaciones en una instancia permiten la reutilización de la memoria del original con solo una pequeña cantidad (típicamente O(1) u O (lg n)) de copia o nueva asignación se llama una estructura de datos inmutable "persistente". Las cadenas en. NET son inmutables; su pregunta es esencialmente "¿por qué no son persistentes?"

Porque cuando nos fijamos en las operaciones que son típicamente hechas en cadenas en programas.NET, es en todos los sentidos relevantes apenas peor en absoluto simplemente hacer una cadena completamente nueva. El gasto y la dificultad de construir un complejo persistente la estructura de datos no se paga sola.

La gente normalmente usa "subcadena" para extraer una cadena corta say digamos, diez o veinte caracteres out de una cadena algo más larga maybe tal vez un par de cientos de caracteres. Tiene una línea de texto en un archivo separado por comas y desea extraer el tercer campo, que es un apellido. La línea será tal vez un par de cientos de caracteres, el nombre será un par de docenas. La asignación de cadenas y la copia de memoria de cincuenta bytes es asombrosamente rápido en hardware moderno. Que hacer una nueva estructura de datos que consiste en un puntero a la mitad de una cadena existente más una longitud es también sorprendentemente rápido es irrelevante; "lo suficientemente rápido" es por definición lo suficientemente rápido.

Las subcadenas extraídas son típicamente pequeñas en tamaño y cortas en vida; el recolector de basura va a recuperarlas pronto, y no ocuparon mucho espacio en el montón en primer lugar. Así que el uso de una estrategia persistente que fomenta la reutilización de la mayor parte de la memoria tampoco es una victoria; todo lo que has hecho es hacer que tu recolector de basura se vuelva más lento porque ahora tiene que preocuparse por manejar punteros interiores.

Si las operaciones de subcadena que la gente normalmente hacía en cadenas eran completamente diferentes, entonces tendría sentido ir con un enfoque persistente. Si las personas típicamente tenían cadenas de millones de caracteres, y estaban extrayendo miles de subcadenas superpuestas con tamaños en el rango de cien mil caracteres, y esas subcadenas vivido mucho tiempo en el montón, entonces tendría perfecto sentido ir con un enfoque de subcadena persistente; sería un desperdicio y una tontería no hacerlo. Pero la mayoría de los programadores de línea de negocio no hacen nada, ni siquiera vagamente como ese tipo de cosas. . NET no es una plataforma que se adapte a las necesidades del Proyecto Genoma Humano; los programadores de análisis de ADN tienen que resolver problemas con esas características de uso de cadenas todos los días; las probabilidades son buenas de que no lo haga. Los pocos que construyen su poseer estructuras de datos persistentes que coincidan estrechamente con sus escenarios de uso.

Por ejemplo, mi equipo escribe programas que hacen análisis sobre la marcha del código C# y VB a medida que lo escribe. Algunos de esos archivos de código son enormes y por lo tanto no podemos estar haciendo O(n) manipulación de cadenas para extraer subcadenas o insertar o eliminar caracteres. Hemos construido un montón de estructuras de datos inmutables persistentes para representar ediciones en un búfer de texto que nos permiten reutilizar rápida y eficientemente la mayor parte de los datos de cadena existentes y los análisis léxicos y sintácticos existentes sobre una edición típica. Este fue un problema difícil de resolver y su solución se adaptó estrechamente al dominio específico de la edición de código C# y VB. Sería poco realista esperar que el tipo de cadena incorporado resuelva este problema para nosotros.

 403
Author: Eric Lippert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-05-08 09:34:06

Precisamente debido a que las cadenas son inmutables, .Substring debe hacer una copia de al menos una porción de la cadena original. Hacer una copia de n bytes debe tomar O(n) tiempo.

¿Cómo crees que sería copiar un montón de bytes en constante tiempo?


EDIT: Mehrdad sugiere no copiar la cadena en absoluto, sino mantener una referencia a una parte de ella.

Considere en. Net, una cadena multi-megabyte, en la que alguien llama .SubString(n, n+3) (para cualquier n en el medio de la cuerda).

Ahora, la cadena COMPLETA no se puede recoger basura solo porque una referencia está sosteniendo 4 caracteres? Eso parece un ridículo desperdicio de espacio.

Además, rastrear referencias a subcadenas (que incluso pueden estar dentro de subcadenas), y tratar de copiar en momentos óptimos para evitar derrotar al GC (como se describió anteriormente), hace que el concepto sea una pesadilla. Es mucho más simple, y más confiable, copiar en .SubString, y mantener el sencillo inmutable modelo.


EDITAR: Aquí hay un buena lectura sobre el peligro de mantener referencias a subcadenas dentro de cadenas más grandes.

 115
Author: abelenky,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-10-20 16:00:45

Java (a diferencia de.NET) proporciona dos formas de hacer Substring(), puede considerar si desea mantener solo una referencia o copiar una subcadena completa a una nueva ubicación de memoria.

El simple .substring(...) comparte el array char utilizado internamente con el objeto String original, que luego con new String(...) puede copiar a un nuevo array, si es necesario (para evitar obstaculizar la recolección de basura del original).

Creo que este tipo de flexibilidad es la mejor opción para un desarrollador.

 32
Author: sll,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-23 23:06:54

Java se utiliza para hacer referencia a cadenas más grandes, pero:

Java cambió su comportamiento a copiando también, para evitar fugas de memoria.

Sin embargo, siento que se puede mejorar: ¿por qué no hacer la copia condicionalmente?

Si la subcadena es al menos la mitad del tamaño del padre, se puede hacer referencia al padre. De lo contrario, uno puede hacer una copia. Esto evita la pérdida de una gran cantidad de memoria, mientras que todavía proporciona un beneficio significativo.

 10
Author: Mehrdad,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-12-03 19:21:17

Ninguna de las respuestas aquí aborda "el problema de los corchetes", es decir que las cadenas en.NET se representan como una combinación de un BStr (la longitud almacenada en la memoria "antes" del puntero) y un CStr (la cadena termina en un '\0').

La cadena "Hello there" se representa como{[15]]}

0B 00 00 00 48 00 65 00 6C 00 6F 00 20 00 74 00 68 00 65 00 72 00 65 00 00 00

(si se asigna a un char* en una instrucción fixed, el puntero apuntaría al 0x48.)

Esta estructura permite una búsqueda rápida de la longitud de una cadena (útil en muchos contextos) y permite que el puntero se pase en una P / Invoke a Win32 (u otras) API que esperan una cadena terminada en null.

Cuando haces Substring(0, 5) la regla "oh, pero prometí que habría un carácter nulo después del último carácter" dice que necesitas hacer una copia. Incluso si tienes la subcadena al final, entonces no habría lugar para poner la longitud sin corromper las otras variables.


A veces, sin embargo, realmente quieres hablar de " la mitad de la cadena", y no necesariamente te importa el comportamiento P / Invoke. La estructura ReadOnlySpan<T> recientemente añadida se puede usar para obtener una subcadena sin copia:

string s = "Hello there";
ReadOnlySpan<char> hello = s.AsSpan(0, 5);
ReadOnlySpan<char> ell = hello.Slice(1, 3);

La "subcadena" ReadOnlySpan<char> almacena la longitud de forma independiente, y no garantiza que haya un '\0' después del final del valor. Se puede usar de muchas maneras "como una cadena", pero no es "una cadena" ya que no tiene características BStr o CStr (mucho menos ambas). Si nunca (directamente) P/Invoca entonces no hay mucho de una diferencia (a menos que la API que desea llamar no tenga una sobrecarga ReadOnlySpan<char>).

ReadOnlySpan<char> no se puede utilizar como el campo de un tipo de referencia, por lo que también hayReadOnlyMemory<char> (s.AsMemory(0, 5)), que es una forma indirecta de tener un ReadOnlySpan<char>, por lo que existen las mismas diferencias-de-string.

Algunas de las respuestas/comentarios en las respuestas anteriores hablaban de que era un desperdicio que el recolector de basura tuviera que mantener una cadena de millones de caracteres mientras se continúa hablando de 5 caracteres. Eso es precisamente el comportamiento que se puede obtener con el enfoque ReadOnlySpan<char>. Si solo estás haciendo cálculos cortos, el enfoque ReadOnlySpan es probablemente mejor. Si necesita persistirlo por un tiempo y va a mantener solo un pequeño porcentaje de la cadena original, hacer una subcadena adecuada (para recortar el exceso de datos) es probablemente mejor. Hay un punto de transición en algún lugar en el medio, pero depende de su uso específico.

 1
Author: bartonjs,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-16 16:21:20