Llegué tarde a estas preguntas y respuestas con excelentes respuestas, pero quería entrometerme como un extranjero acostumbrado a mirar las cosas desde el punto de vista de nivel inferior de bits y bytes en la memoria.
Estoy muy entusiasmado con los diseños inmutables, incluso desde una perspectiva C, y desde la perspectiva de encontrar nuevas formas de programar efectivamente este hardware bestial que tenemos en estos días.
Más lento / más rápido
En cuanto a la pregunta de si hace las cosas más lentas, sería una respuesta robótica yes
. En este tipo de nivel conceptual muy técnico, la inmutabilidad solo podría hacer las cosas más lentas. El hardware funciona mejor cuando no está asignando esporádicamente memoria y puede simplemente modificar la memoria existente en su lugar (por qué tenemos conceptos como localidad temporal).
Sin embargo, una respuesta práctica es maybe
. El rendimiento sigue siendo en gran medida una métrica de productividad en cualquier base de código no trivial. Por lo general, no encontramos que las bases de código horribles para mantener las condiciones de carrera sean las más eficientes, incluso si ignoramos los errores. La eficiencia es a menudo una función de elegancia y simplicidad. El pico de micro optimizaciones puede entrar en conflicto, pero generalmente están reservadas para las secciones de código más pequeñas y críticas.
Transformando bits y bytes inmutables
Viniendo desde el punto de vista de bajo nivel, si conceptos como que de rayos X objects
y strings
y así sucesivamente, en el corazón de la misma es sólo bits y bytes en diversas formas de memoria con diferentes características de velocidad / tamaño (velocidad y el tamaño de hardware de memoria siendo típicamente mutuamente excluyentes).
A la jerarquía de memoria de la computadora le gusta cuando accedemos repetidamente a la misma porción de memoria, como en el diagrama anterior, ya que mantendrá esa porción de memoria a la que se accede con frecuencia en la forma más rápida de memoria (caché L1, por ejemplo, que es casi tan rápido como un registro). Podríamos acceder repetidamente a la misma memoria (reutilizándola varias veces) o acceder repetidamente a diferentes secciones del fragmento (por ejemplo, recorrer los elementos en un fragmento contiguo que accede repetidamente a varias secciones de ese fragmento de memoria).
Terminamos lanzando una llave inglesa en ese proceso si la modificación de esta memoria termina deseando crear un bloque de memoria completamente nuevo en el lateral, así:
... en este caso, acceder al nuevo bloque de memoria podría requerir fallas de página obligatorias y errores de caché para volver a colocarlo en las formas más rápidas de memoria (hasta un registro). Eso puede ser un verdadero asesino de rendimiento.
Hay formas de mitigar esto, sin embargo, utilizando un grupo de reserva de memoria preasignada, ya tocada.
Grandes agregados
Otro problema conceptual que surge de una vista de nivel ligeramente superior es simplemente hacer copias innecesarias de agregados realmente grandes a granel.
Para evitar un diagrama demasiado complejo, imaginemos que este simple bloque de memoria es de alguna manera costoso (quizás caracteres UTF-32 en un hardware increíblemente limitado).
En este caso, si quisiéramos reemplazar "AYUDA" con "KILL" y este bloque de memoria fuera inmutable, tendríamos que crear un bloque completamente nuevo en su totalidad para crear un nuevo objeto único, aunque solo hayan cambiado partes de él. :
Estirando un poco nuestra imaginación, este tipo de copia profunda de todo lo demás solo para hacer que una pequeña parte sea única podría ser bastante costoso (en casos del mundo real, este bloque de memoria sería mucho, mucho más grande para plantear un problema).
Sin embargo, a pesar de tal gasto, este tipo de diseño tenderá a ser mucho menos propenso al error humano. Cualquiera que haya trabajado en un lenguaje funcional con funciones puras probablemente pueda apreciar esto, y especialmente en los casos de subprocesos múltiples donde podemos multiprocesar dicho código sin preocuparnos en el mundo. En general, los programadores humanos tienden a tropezar con los cambios de estado, especialmente los que causan efectos secundarios externos a estados fuera del alcance de una función actual. Incluso recuperarse de un error externo (excepción) en tal caso puede ser increíblemente difícil con cambios de estado externo mutable en la mezcla.
Una forma de mitigar este trabajo de copia redundante es hacer que estos bloques de memoria se conviertan en una colección de punteros (o referencias) a caracteres, de esta manera:
Disculpas, no me di cuenta de que no necesitamos hacer algo L
único al hacer el diagrama.
El azul indica datos copiados poco profundos.
... desafortunadamente, esto sería increíblemente costoso para pagar un puntero / costo de referencia por personaje. Además, podríamos dispersar el contenido de los caracteres por todo el espacio de direcciones y terminar pagándolo en forma de un montón de fallas de página y errores de caché, lo que hace que esta solución sea aún peor que copiar todo en su totalidad.
Incluso si fuéramos cuidadosos al asignar estos caracteres de manera contigua, digamos que la máquina puede cargar 8 caracteres y 8 punteros a un carácter en una línea de caché. Terminamos cargando memoria como esta para atravesar la nueva cadena:
En este caso, terminamos requiriendo 7 líneas diferentes de caché de memoria contigua para cargar esta cadena, cuando idealmente solo necesitamos 3.
Trocear los datos
Para mitigar el problema anterior, podemos aplicar la misma estrategia básica pero a un nivel más grueso de 8 caracteres, p. Ej.
El resultado requiere que se carguen 4 líneas de caché de datos (1 para los 3 punteros y 3 para los caracteres) para atravesar esta cadena que está a solo 1 menos del óptimo teórico.
Así que eso no está nada mal. Hay un poco de pérdida de memoria, pero la memoria es abundante y usar más no ralentiza las cosas si la memoria adicional va a ser datos fríos a los que no se accede con frecuencia. Es solo para los datos calientes y contiguos donde el uso reducido de la memoria y la velocidad a menudo van de la mano donde queremos colocar más memoria en una sola página o línea de caché y acceder a todo antes del desalojo. Esta representación es bastante amigable con el caché.
Velocidad
Por lo tanto, utilizar una representación como la anterior puede proporcionar un equilibrio de rendimiento bastante decente. Probablemente, los usos más críticos para el rendimiento de las estructuras de datos inmutables tomarán esta naturaleza de modificar piezas de datos gruesas y hacerlas únicas en el proceso, mientras copian piezas poco modificadas. También implica cierta sobrecarga de operaciones atómicas para hacer referencia a las piezas copiadas poco profundas de forma segura en un contexto multiproceso (posiblemente con algún recuento de referencia atómica en curso).
Sin embargo, siempre que estos fragmentos de datos estén representados en un nivel lo suficientemente grueso, gran parte de esta sobrecarga disminuye y posiblemente incluso se trivializa, al tiempo que nos brinda la seguridad y la facilidad de codificar y multiprocesar más funciones en una forma pura sin un lado externo. efectos
Mantener datos nuevos y antiguos
Donde veo la inmutabilidad como potencialmente más útil desde el punto de vista del rendimiento (en un sentido práctico) es cuando podemos sentir la tentación de hacer copias completas de datos grandes para que sean únicos en un contexto mutable donde el objetivo es producir algo nuevo a partir de algo que ya existe de una manera en la que queremos mantener tanto lo nuevo como lo antiguo, cuando podríamos hacer pequeños pedazos únicos con un diseño cuidadoso e inmutable.
Ejemplo: Deshacer sistema
Un ejemplo de esto es un sistema de deshacer. Podríamos cambiar una pequeña porción de una estructura de datos y queremos mantener tanto el formulario original que podemos deshacer como el nuevo formulario. Con este tipo de diseño inmutable que solo hace que las secciones pequeñas y modificadas de la estructura de datos sean únicas, simplemente podemos almacenar una copia de los datos antiguos en una entrada de deshacer mientras solo pagamos el costo de memoria de los datos de porciones únicas agregadas. Esto proporciona un equilibrio muy efectivo de productividad (haciendo que la implementación de un sistema de deshacer sea pan comido) y el rendimiento.
Interfaces de alto nivel
Sin embargo, algo incómodo surge con el caso anterior. En un tipo de contexto de función local, los datos mutables son a menudo los más fáciles y más fáciles de modificar. Después de todo, la forma más fácil de modificar una matriz a menudo es simplemente recorrerla y modificar un elemento a la vez. Podemos terminar aumentando la sobrecarga intelectual si tuviéramos una gran cantidad de algoritmos de alto nivel para elegir para transformar una matriz y tuviéramos que elegir el apropiado para asegurarnos de que todas estas copias superficiales gruesas se realicen mientras las partes que se modifican son hecho único
Probablemente, la forma más fácil en esos casos es usar buffers mutables localmente dentro del contexto de una función (donde generalmente no nos hacen tropezar) que cometen cambios atómicamente en la estructura de datos para obtener una nueva copia inmutable (creo que algunos idiomas llaman estos "transitorios") ...
... o podríamos simplemente modelar funciones de transformación de nivel superior e superior sobre los datos para poder ocultar el proceso de modificación de un búfer mutable y comprometerlo en la estructura sin una lógica mutable involucrada. En cualquier caso, este aún no es un territorio ampliamente explorado, y tenemos que cortar nuestro trabajo si adoptamos diseños inmutables más para crear interfaces significativas sobre cómo transformar estas estructuras de datos.
Estructuras de datos
Otra cosa que surge aquí es que la inmutabilidad utilizada en un contexto crítico para el rendimiento probablemente querrá que las estructuras de datos se descompongan en datos gruesos donde los fragmentos no son demasiado pequeños pero tampoco demasiado grandes.
Las listas vinculadas pueden querer cambiar un poco para acomodar esto y convertirse en listas no desarrolladas. Las matrices contiguas grandes pueden convertirse en una matriz de punteros en trozos contiguos con indexación de módulo para acceso aleatorio.
Potencialmente cambia la forma en que vemos las estructuras de datos de una manera interesante, al tiempo que empuja las funciones de modificación de estas estructuras de datos para que se parezcan a una naturaleza más voluminosa para ocultar la complejidad adicional de copiar algunos bits aquí y hacer que otros bits sean únicos allí.
Actuación
De todos modos, esta es mi pequeña visión de nivel inferior sobre el tema. Teóricamente, la inmutabilidad puede tener un costo que varía de muy grande a más pequeño. Pero un enfoque muy teórico no siempre hace que las aplicaciones sean rápidas. Puede que sean escalables, pero la velocidad del mundo real a menudo requiere adoptar una mentalidad más práctica.
Desde una perspectiva práctica, cualidades como el rendimiento, el mantenimiento y la seguridad tienden a convertirse en un gran desenfoque, especialmente para una base de código muy grande. Si bien el rendimiento en cierto sentido absoluto se degrada con la inmutabilidad, es difícil argumentar los beneficios que tiene para la productividad y la seguridad (incluida la seguridad de los hilos). Con un aumento de estos, a menudo puede venir un aumento en el rendimiento práctico, aunque solo sea porque los desarrolladores tienen más tiempo para ajustar y optimizar su código sin ser abrumados por errores.
Entonces, desde este sentido práctico, creo que las estructuras de datos inmutables podrían ayudar al rendimiento en muchos casos, por extraño que parezca. Un mundo ideal podría buscar una combinación de estos dos: estructuras de datos inmutables y mutables, siendo los muy mutables muy seguros de usar en un ámbito muy local (ej .: local a una función), mientras que los inmutables pueden evitar el lado externo. efectúa directamente y convierte todos los cambios en una estructura de datos en una operación atómica que produce una nueva versión sin riesgo de condiciones de carrera.