Translate

jueves, 13 de julio de 2023

Manejo de memoria en Rust

 


Tradicionalmente, los Lenguajes se han dividido en dos grandes categorías:

  • Control total a través de la gestión manual de la memoria: C, C++, Pascal, …
  • Seguridad total a través de la gestión automática de la memoria en tiempo de ejecución: Java, Python, Go, Haskell, …

Rust ofrece una nueva mezcla:

Control total y seguridad a través de la aplicación en tiempo de compilación de la gestión correcta de la memoria.

Lo hace con un concepto de propiedad explícito.

Primero, actualicemos cómo funciona la administración de memoria.

Stack: 

  • Área continua de memoria para variables locales.
  • Los valores tienen tamaños fijos conocidos en tiempo de compilación.
  • Extremadamente rápido: simplemente mueva un puntero de pila.
  • Fácil de administrar: sigue las llamadas de función.
  • Gran recuerdo localidad.

Heap o monticulo : 

  • almacenamiento de valores fuera de las llamadas a funciones.
  • Los valores tienen tamaños dinámicos determinados en tiempo de ejecución.
  • Ligeramente más lento que la pila: se necesita algo de contabilidad.
  • No hay garantía de localidad de memoria.

La creación de una cadena coloca datos de tamaño fijo en la pila o stack y datos de tamaño dinámico en el monticulo o Heap :

fn main() {
    let s1 = String::from("Hello");
}



Un String está respaldado por un Vec, por lo que tiene una capacidad y longitud y puede crecer si es mutable a través de la reasignación en el Heap.

Podemos inspeccionar el diseño de la memoria con código inseguro. Sin embargo, debe señalar que esto es legítimamente inseguro.

fn main() {
    let mut s1 = String::from("Hello");
    s1.push(' ');
    s1.push_str("world");
    
    unsafe {
        let (capacity, ptr, len): (usize, usize, usize) = std::mem::transmute(s1);
        println!("ptr = {ptr:#x}, len = {len}, capacity = {capacity}");
    }
}

Veamos como son las formas que tienen de gestionar la memoria los lenguajes: 

Gestión de memoria manual

Usted mismo asigna y desasigna la memoria del heap. Se llama Gestión de memoria manual

Si no se hace con cuidado, esto puede provocar bloqueos, errores, vulnerabilidades de seguridad y pérdidas de memoria.

Ejemplo C, debe llamar a  free cada vez que asigno un puntero con malloc:

void foo(size_t n) {

    int* int_array = malloc(n * sizeof(int));

    //

    // ... lots of code

    //

    free(int_array);

}

Si la función termina entre malloc y free: el puntero se pierde y no podemos desasignar la memoria. Esto es un Memory leaked. 

Administración de memoria basada en alcance o ámbito

Los constructores y destructores te permiten engancharte a la vida útil de un objeto.

Al envolver un puntero en un objeto, puede liberar memoria cuando se destruye el objeto. El compilador garantiza que esto suceda, incluso si se genera una excepción.

Esto a menudo se denomina adquisición de recursos es inicialización (RAII) y le brinda indicadores inteligentes.

Un ejemplo de C++

void say_hello(std::unique_ptr<Person> person) {

  std::cout << "Hello " << person->name << std::endl;

}

  • El objeto std::unique_ptr se asigna en la pila y apunta a la memoria asignada en el heap.
  • Al final de say_hello, se ejecutará el destructor std::unique_ptr.
  • El destructor libera el objeto Person al que apunta.
Los constructores de movimientos especiales se usan cuando se pasa la propiedad a una función:

std::unique_ptr<Person> person = find_person("Carla");
say_hello(std::move(person));

Gestión automática de memoria

Una alternativa a la gestión de memoria manual y basada en el ámbito es la gestión de memoria automática:

  • El programador nunca asigna o desasigna memoria explícitamente.
  • Un recolector de basura encuentra memoria no utilizada y la desasigna para el programador.

Ejemplo Java, el objeto persona no se desasigna después de que sayHello devuelve:

void sayHello(Person person) {
  System.out.println("Hello " + person.getName());
}

Gestión de memoria en Rust

La gestión de la memoria en Rust es una combinación:

  • Seguro y correcto como Java, pero sin un recolector de basura.
  • Según la abstracción (o combinación de abstracciones) que elija, puede ser un único puntero único, referencia contada o atómicamente referencia contada.
  • Basado en el alcance como C++, pero el compilador impone una adherencia total.
  • Un usuario de Rust puede elegir la abstracción correcta para la situación, algunos incluso no tienen costo en tiempo de ejecución como C.
Rust logra esto modelando la propiedad o ownership explícitamente.