Compartir a través de


Reducción de las asignaciones de memoria mediante nuevas características de C#

Importante

Las técnicas descritas en esta sección mejoran el rendimiento cuando se aplican a las rutas de acceso activas en el código. Las rutas de acceso activas son las secciones del código base que se ejecutan a menudo y repetidamente en operaciones normales. La aplicación de estas técnicas al código que no se ejecuta a menudo tendrá un impacto mínimo. Antes de realizar cambios para mejorar el rendimiento, es fundamental medir una línea base. A continuación, analice esa línea de base para determinar dónde se producen los cuellos de botella de memoria. Puede obtener información sobre muchas herramientas multiplataforma para medir el rendimiento de la aplicación en la sección diagnóstico e instrumentación. Puede practicar una sesión de generación de perfiles en el tutorial para medir el uso de memoria en la documentación de Visual Studio.

Una vez que haya medido el uso de memoria y haya determinado que puede reducir las asignaciones, use las técnicas de esta sección para reducir las asignaciones. Después de cada cambio sucesivo, mida de nuevo el uso de memoria. Asegúrese de que cada cambio tiene un impacto positivo en el uso de memoria en la aplicación.

El trabajo de rendimiento en .NET suele significar eliminar las asignaciones de memoria de tu código. Cada bloque de memoria que asigne debe liberarse finalmente. Menos asignaciones reducen el tiempo invertido en la recolección de elementos no utilizados. Facilita un tiempo de ejecución más predecible al eliminar la recolección de elementos no utilizados de trayectorias de código específicas.

Una táctica común para reducir las asignaciones es cambiar las estructuras de datos críticas de tipos de class a tipos de struct. Este cambio afecta a la semántica del uso de esos tipos. Los parámetros y los resultados ahora se pasan por valor en lugar de por referencia. El costo de copiar un valor es insignificante si los tipos son pequeños, tres palabras o menos (teniendo en cuenta que una palabra es de tamaño natural de un entero). Es medible y puede tener un impacto real en el rendimiento de los tipos más grandes. Para combatir el efecto de la copia, los desarrolladores pueden pasar estos tipos mediante ref para obtener la semántica prevista.

Las características de C# ref ofrecen la capacidad de expresar la semántica deseada para struct los tipos sin afectar negativamente a su facilidad de uso general. Antes de estas mejoras, los desarrolladores necesitaban recurrir a construcciones unsafe con punteros y memoria sin procesar para lograr el mismo impacto en el rendimiento. El compilador genera código seguro verificable para las nuevas ref características relacionadas. El código seguro verificable significa que el compilador detecta posibles saturaciones de búfer o acceso a memoria sin asignar o libre. El compilador detecta y evita algunos errores.

Pasar y devolver por referencia

Las variables en C# almacenan valores. En struct los tipos, el valor es el contenido de una instancia del tipo . En class los tipos, el valor es una referencia a un bloque de memoria que almacena una instancia del tipo . Agregar el ref modificador significa que la variable almacena la referencia al valor. En los tipos struct, la referencia apunta al almacenamiento que contiene el valor. En los tipos class, la referencia apunta al almacenamiento que contiene la referencia al bloque de memoria.

En C#, los parámetros de los métodos se pasan por valor y los valores de retorno se devuelven por valor. El valor del argumento se pasa al método . El valor del argumento return es el valor devuelto.

El ref, in, ref readonly, o out modificador indica que el argumento se pasa por referencia. Se pasa al método una referencia a la ubicación de almacenamiento. Agregar ref a la firma del método significa que el valor devuelto se devuelve por referencia. Una referencia a la ubicación de almacenamiento es el valor devuelto.

También puede usar la asignación de referencias para que una variable haga referencia a otra variable. Una asignación típica copia el valor del lado derecho en la variable del lado izquierdo de la asignación. Una asignación de referencia copia la ubicación de memoria de la variable en el lado derecho a la variable del lado izquierdo. Ahora ref hace referencia a la variable original:

int anInteger = 42; // assignment.
ref int location = ref anInteger; // ref assignment.
ref int sameLocation = ref location; // ref assignment

Console.WriteLine(location); // output: 42

sameLocation = 19; // assignment

Console.WriteLine(anInteger); // output: 19

Al asignar una variable, se cambia su valor. Cuando se asigna por referencia una variable, se cambia aquello a lo que hace referencia.

Puede trabajar directamente con el almacenamiento para los valores mediante variables ref, pasar por referencia y asignar por referencia. Las reglas de ámbito aplicadas por el compilador garantizan la seguridad al trabajar directamente con el almacenamiento.

Los modificadores ref readonly y in ambos indican que el argumento debe pasarse por referencia y no se puede reasignar en el método. La diferencia es que ref readonly indica que el método usa el parámetro como una variable. El método puede capturar el parámetro o puede devolver el parámetro por referencia de solo lectura. En esos casos, debe usar el modificador ref readonly. De lo contrario, el in modificador ofrece más flexibilidad. No es necesario agregar el in modificador a un argumento para un in parámetro, por lo que puede actualizar las firmas de API existentes de forma segura mediante el in modificador . El compilador emite una advertencia si no agrega los modificadores ref o in a un argumento para un parámetro ref readonly.

Contexto seguro para referencia

C# incluye reglas para ref expresiones para asegurarse de que no se puede tener acceso a una ref expresión donde el almacenamiento al que hace referencia ya no es válido. Considere el ejemplo siguiente:

public ref int CantEscape()
{
    int index = 42;
    return ref index; // Error: index's ref safe context is the body of CantEscape
}

El compilador notifica un error porque no se puede devolver una referencia a una variable local desde un método . El autor de la llamada no puede acceder al almacenamiento al que se hace referencia. El contexto seguro ref define el ámbito en el que una ref expresión es segura para acceder o modificar. En la tabla siguiente se enumeran los contextos seguros de referencia para los tipos de variable. ref los campos no se pueden declarar en un class ni en un struct sin referencia, por lo que esas filas no están en la tabla:

Declaración ref contexto seguro
local no por referencia bloque donde se declara una local
parámetro no por referencia método actual
ref, ref readonly, in parámetros método de llamada
Parámetro out método actual
Campo class método de llamada
campo sin referencia struct método actual
Campo ref de ref struct método de llamada

Se puede devolver ref una variable si su contexto seguro para referencia es el método que realiza la llamada. Si su contexto seguro para referencia es el método actual o un bloque, no se permite la devolución ref. En el fragmento de código siguiente se muestran dos ejemplos. Se puede acceder a un campo miembro desde el ámbito que invoca un método, por lo que el ámbito seguro de referencia de un campo de clase o estructura es el método que llama. El contexto seguro para referencia para un parámetro con los modificadores ref o in es todo el método. Ambos se pueden devolver ref desde un método miembro.

private int anIndex;

public ref int RetrieveIndexRef()
{
    return ref anIndex;
}

public ref int RefMin(ref int left, ref int right)
{
    if (left < right)
        return ref left;
    else
        return ref right;
}

Nota:

Cuando el ref readonly modificador o in se aplica a un parámetro , ese parámetro se puede devolver mediante ref readonly, no ref.

El compilador garantiza que una referencia no pueda escapar su contexto seguro ref. Puede usar ref parámetros, ref returny ref variables locales de forma segura porque el compilador detecta si ha escrito accidentalmente código en el que se puede tener acceso a una ref expresión cuando su almacenamiento no es válido.

Contexto seguro y estructuras de referencia

ref struct Los tipos requieren más reglas para asegurarse de que se pueden usar de forma segura. Un ref struct tipo puede incluir ref campos. Esto requiere la introducción de un contexto seguro. Para la mayoría de los tipos, el contexto seguro es el método de llamada. Es decir, un valor que no es un ref struct elemento siempre se puede devolver desde un método .

Informalmente, el contexto seguro de un ref struct es el ámbito al que se puede acceder a todos sus ref campos. En otras palabras, es la intersección del contexto seguro para referencia de todos sus campos ref. El método siguiente devuelve un objeto ReadOnlySpan<char> a un campo miembro, por lo que su contexto seguro es el método :

private string longMessage = "This is a long message";

public ReadOnlySpan<char> Safe()
{
    var span = longMessage.AsSpan();
    return span;
}

Por el contrario, el código siguiente emite un error porque el miembro ref field de Span<int> se refiere a la matriz de enteros asignada en la pila. No puede aplicar escape al método:

public Span<int> M()
{
    int length = 3;
    Span<int> numbers = stackalloc int[length];
    for (var i = 0; i < length; i++)
    {
        numbers[i] = i;
    }
    return numbers; // Error! numbers can't escape this method.
}

Unificar tipos de memoria

La introducción de System.Span<T> y System.Memory<T> proporciona un modelo unificado para trabajar con memoria. System.ReadOnlySpan<T> y System.ReadOnlyMemory<T> proporcionan versiones de solo lectura para acceder a la memoria. Todas proporcionan una abstracción sobre un bloque de memoria que almacena una matriz de elementos similares. La diferencia es que Span<T> y ReadOnlySpan<T> son tipos ref struct, mientras que Memory<T> y ReadOnlyMemory<T> son tipos struct. Los intervalos contienen un ref field. Por lo tanto, las instancias de un objeto span no pueden dejar su contexto seguro. El contexto seguro de un ref struct es el contexto ref seguro de su ref field. La implementación de Memory<T> y ReadOnlyMemory<T> elimina esta restricción. Estos tipos se usan para acceder directamente a los búferes de memoria.

Mejorar el rendimiento con seguridad para referencia

El uso de estas características para mejorar el rendimiento implica estas tareas:

  • Evitar asignaciones: Cuando cambias un tipo de class a un struct, cambias la forma en que se almacena. Las variables locales se almacenan en la pila. Los miembros se almacenan en línea cuando se asigna el objeto contenedor. Este cambio significa menos asignaciones, lo que reduce el trabajo que realiza el recolector de basura. También puede disminuir la presión de memoria para que el recolector de basura se ejecute con menos frecuencia.
  • Conservar la semántica de referencia: cambiar un tipo de class a a struct cambia la semántica de pasar una variable a un método. El código que modificó el estado de sus parámetros necesita modificaciones. Ahora que el parámetro es , structel método modifica una copia del objeto original. Puede restaurar la semántica original pasando ese parámetro como parámetro ref . Después de ese cambio, el método modifica de nuevo el original struct .
  • Evitar copiar datos: la copia de tipos struct más grandes puede afectar al rendimiento en algunas rutas de acceso de código. También puede agregar el ref modificador para pasar estructuras de datos más grandes a métodos por referencia en lugar de por valor.
  • Restringir modificaciones: cuando se pasa un struct tipo por referencia, el método llamado podría modificar el estado de la estructura. Puede reemplazar el modificador ref con los modificadores ref readonly o in para indicar que el argumento no se puede modificar. Se prefiere ref readonly cuando el método captura el parámetro o lo devuelve por referencia de solo lectura. También puede crear tipos readonly struct o struct con miembros readonly para proporcionar más control sobre los miembros de un struct que se pueden modificar.
  • Manipular directamente la memoria: algunos algoritmos son más eficaces al tratar estructuras de datos como un bloque de memoria que contiene una secuencia de elementos. Los tipos Span y Memory proporcionan acceso seguro a bloques de memoria.

Ninguna de estas técnicas requiere unsafe código. Se usa sabiamente, puede obtener características de rendimiento del código seguro que anteriormente solo era posible mediante técnicas no seguras. Puede probar las técnicas en el tutorial sobre la reducción de las asignaciones de memoria.