¿Por qué las cuerdas son tan lentas?

23

Desde mi primera clase de programación en la escuela secundaria, he escuchado que las operaciones con cuerdas son más lentas, es decir, más costosas, que la mítica "operación promedio". ¿Por qué los hace tan lentos? (Esta pregunta se dejó intencionalmente amplia).

Estallidos
fuente
11
Si sabe que estas "operaciones promedio" son míticas, ¿puede al menos decirnos cuáles son algunas de ellas? Dado que está haciendo una pregunta tan vaga, es difícil confiar en su afirmación de que estas operaciones no especificadas realmente son míticas.
seh
1
@seh, desafortunadamente, en realidad no puedo responder eso. Las pocas veces que le he preguntado a la gente qué cuerdas son más lentas, simplemente se encogen de hombros y dicen "son lentas". Además, si tuviera información más específica, esta sería una pregunta para SO, no para programadores; ya es un poco limítrofe.
Aparece el
Cual es el punto ? Si las cadenas indicadas son realmente lentas, ¿dejará de usarlas?
Tulains Córdova
Olvídalo. Si alguien te dice esas tonterías, la contra-pregunta es: "¿En serio? ¿Lo son? ¿Deberíamos usar un int-array entonces?"
Ingo

Respuestas:

47

La "operación promedio" tiene lugar en primitivas. Pero incluso en los idiomas en los que las cadenas se tratan como primitivas, siguen siendo matrices debajo del capó, y hacer cualquier cosa que involucre a toda la cadena lleva tiempo O (N), donde N es la longitud de la cadena.

Por ejemplo, agregar dos números generalmente requiere de 2 a 4 instrucciones ASM. Concatenar ("agregar") dos cadenas requiere una nueva asignación de memoria y copias de una o dos cadenas, involucrando la cadena completa.

Ciertos factores del lenguaje pueden empeorarlo. En C, por ejemplo, una cadena es simplemente un puntero a una matriz de caracteres con terminación nula. Esto significa que no sabe cuánto dura, por lo que no hay forma de optimizar un ciclo de copia de cadenas con operaciones de movimiento rápido; necesita copiar un carácter a la vez para poder probar cada byte para el terminador nulo.

Mason Wheeler
fuente
44
Y ciertos idiomas lo hacen mucho mejor: la codificación de Delphi de la longitud de la cadena al comienzo de la matriz hace que la concatenación de cadenas sea muy rápida.
Frank Shearar
44
@gablin: También ayuda al hacer que la cadena se copie mucho más rápido. Cuando conoce el tamaño por adelantado, no tiene que copiar un byte a la vez y verificar que cada byte tenga un terminador nulo, por lo que puede usar el tamaño completo de cualquier registro, incluidos los SIMD, para el movimiento de datos. hasta 16 veces más rápido
Mason Wheeler
44
@mathepic: Sí, y eso está bien en la medida en que te lleve, pero cuando comienzas a interactuar con libc u otro código externo, espera un char*, no un strbuf, y vuelves al punto 1. Solo hay tanto puede hacer cuando un mal diseño se incorpora al lenguaje.
Mason Wheeler
66
@mathepic: Por supuesto, el bufpuntero está ahí. Nunca quise dar a entender que no está disponible; más bien, que es necesario. Cualquier código que no sepa acerca de su tipo de cadena optimizado pero no estándar, incluidas cosas tan fundamentales como la biblioteca estándar , todavía tiene que recurrir a lo lento, inseguro char*. Puede llamar a ese FUD si lo desea, pero eso no hace que no sea cierto.
Mason Wheeler
77
Gente, hay una columna de Joel Spolsky sobre el punto de Frank Shearer: Volver a lo básico
usuario16764
14

Este es un hilo viejo y creo que las otras respuestas son geniales, pero pasan por alto algo, así que aquí están mis (tardíos) 2 centavos.

La complejidad sintética del revestimiento de azúcar oculta

El problema con las cadenas es que son ciudadanos de segunda clase en la mayoría de los idiomas y, de hecho, la mayoría de las veces no son realmente parte de la especificación del lenguaje en sí: son una construcción implementada en la biblioteca con algún recubrimiento de azúcar sintáctico ocasional en la parte superior para que sean menos dolorosos de usar.

La consecuencia directa de esto es que el lenguaje oculta una gran parte de su complejidad fuera de su vista, y usted paga los efectos secundarios furtivos porque se acostumbra a considerarlos como una entidad atómica de bajo nivel, al igual que otros tipos primitivos (como se explica en la respuesta más votada y otros).

Detalles de implementacion

Good Ol 'Array

Uno de los elementos de esta "complejidad" subyacente es que la mayoría de las implementaciones de cadenas recurrirían al uso de una estructura de datos simple con algo de espacio de memoria contiguo para representar la cadena: su buena matriz.

Esto tiene sentido, ya que desea que el acceso a la cadena en su conjunto sea rápido. Pero eso implica costos potencialmente terribles cuando desea manipular esta cadena. Acceder a un elemento en el medio podría ser rápido si sabe qué índice está buscando , pero buscar un elemento basado en una condición no lo es.

Incluso devolver el tamaño de la cadena puede ser costoso, si su idioma no almacena en caché la longitud de la cadena y necesita ejecutarla para contar caracteres.

Por razones similares, agregar elementos a su cadena resultará costoso ya que lo más probable es que necesite reasignar algo de memoria para que esta operación ocurra.

Por lo tanto, diferentes idiomas adoptan diferentes enfoques para estos problemas. Java, por ejemplo, se tomó la libertad de hacer que sus cadenas sean inmutables por algunas razones válidas (longitud de almacenamiento en caché, seguridad de subprocesos) y sus contrapartes mutables (StringBuffer y StringBuilder) optarán por asignar el tamaño utilizando trozos de mayor tamaño para no tener que asignarlos. cada vez, pero más bien esperamos los mejores escenarios. Generalmente funciona bien, pero el inconveniente es que a veces se paga por los impactos en la memoria.

Soporte Unicode

Además, y de nuevo esto se debe al hecho de que la capa de azúcar sintáctica de su idioma lo oculta para que juegue bien, a menudo no lo considera términos de soporte Unicode (especialmente mientras no lo necesite realmente y golpear esa pared). Y algunos lenguajes, siendo progresistas, no implementan cadenas con matrices subyacentes de primitivas char simples de 8 bits. Se hornearon en UTF-8 o UTF-16 o lo que tiene soporte para usted, y la consecuencia es un consumo de memoria tremendamente mayor, que a menudo no es necesario, y un mayor tiempo de procesamiento para asignar memoria, procesar las cadenas, e implementar toda la lógica que va de la mano con la manipulación de puntos de código.


El resultado de todo esto es que cuando haces algo equivalente en pseudocódigo para:

hello = "hello,"
world = " world!"
str = hello + world

Puede que no sea, a pesar de todos los mejores esfuerzos que los desarrolladores de lenguaje pusieron para que se comporten como lo haría excepto, un simple como:

a = 1;
b = 2;
shouldBeThree = a + b

Como seguimiento, es posible que desee leer:

haylem
fuente
Buena adición a la discusión actual.
Abel
Me acabo de dar cuenta de que esta es la mejor respuesta porque la declaración mítica que se puede aplicar a cualquier cosa como el cifrado RSA es lenta. La única razón por la que se coloca la cadena en este lugar vergonzoso es porque el operador plus proporcionó cadenas en la mayoría de los idiomas, lo que hace que los novatos no sean conscientes del costo detrás de la operación.
Codismo
@Abel: gracias, me pareció que había espacio para más detalles genéricos.
haylem
@Codism: gracias, me alegra que te haya gustado. De hecho, creo que esto se puede aplicar a muchos casos en los que es solo una cuestión de complejidad estar oculto (y de que ya no prestemos tanta atención a los detalles de nivel inferior hasta que finalmente lo necesitemos porque nos topamos con un cuello de botella o un muro de ladrillos de algún tipo )
haylem
1

La frase "operación promedio" es probablemente una abreviatura para una sola operación de una máquina teórica de Programa almacenado de acceso aleatorio . Esta es la máquina teórica que se usa habitualmente para analizar el tiempo de ejecución de varios algoritmos.

Las operaciones genéricas normalmente se toman para cargar, sumar, restar, almacenar, ramificar. Quizás también lea, imprima y pare.

Pero la mayoría de las operaciones de cadena requieren varias de estas operaciones fundamentales. Por ejemplo, duplicar una cadena normalmente requiere una operación de copia y, por lo tanto, una serie de operaciones que es proporcional a la longitud de una cadena (es decir, es "lineal"). Encontrar una subcadena dentro de otra cadena también tiene una complejidad lineal.

James Youngman
fuente
1

Depende completamente de la operación, cómo se representan las cadenas y qué optimizaciones existen. Si las cadenas tienen 4 u 8 bytes de longitud (y están alineadas), no serían necesariamente más lentas; muchas operaciones serían tan rápidas como las primitivas. O bien, si todas las cadenas tienen un hash de 32 o 64 bits, muchas operaciones también serían igual de rápidas (aunque pague el costo de hash por adelantado).

También depende de lo que quieras decir con "lento". La mayoría de los programas procesarán cadenas con mucha rapidez para lo que se necesita. Las comparaciones de cadenas pueden no ser tan rápidas como comparar dos entradas, pero solo el perfil revelará lo que significa "lento" para su programa.

Kevin Hsu
fuente
0

Déjame responder tu pregunta con una pregunta. ¿Por qué decir una cadena de palabras lleva más tiempo que decir una sola palabra?

ChaosPandion
fuente
2
No necesariamente
user16764
3
Supercalifragilisticexpialidocious
Spoike
s / word / syllable / g
Caleb
Permítame responder su pregunta-respuesta con una pregunta: ¿por qué no dice lo que su respuesta significa? Después de todo, no está claro cómo se puede interpretar que se aplica a algún sistema de tiempo de ejecución.
PJTraill