Udostępnij za pośrednictwem


Zmniejszanie alokacji pamięci przy użyciu nowych funkcji języka C#

Ważne

Techniki opisane w tej sekcji zwiększają wydajność w przypadku zastosowania do ścieżek gorących w kodzie. Gorące ścieżki to te sekcje kodu źródłowego, które są wykonywane często i wielokrotnie w normalnych operacjach. Zastosowanie tych technik do kodu, który nie jest często wykonywany, będzie miało minimalny wpływ. Przed wprowadzeniem jakichkolwiek zmian w celu zwiększenia wydajności kluczowe jest mierzenie punktu odniesienia. Następnie przeanalizuj stan bazowy, aby określić, gdzie występują problemy z pamięcią. Informacje na temat wielu narzędzi międzyplatformowych umożliwiają mierzenie wydajności aplikacji w sekcji Diagnostyka i instrumentacja. Możesz przećwiczyć sesję profilowania w samouczku w celu mierzenia użycia pamięci w dokumentacji programu Visual Studio.

Po zmierzeniu użycia pamięci i ustaleniu, że można zmniejszyć alokacje, użyj technik w tej sekcji, aby zmniejszyć alokacje. Po każdej kolejnej zmianie ponownie zmierz użycie pamięci. Upewnij się, że każda zmiana ma pozytywny wpływ na użycie pamięci w aplikacji.

Praca z wydajnością na platformie .NET często oznacza usunięcie alokacji z kodu. Każdy przydzielony blok pamięci musi zostać ostatecznie zwolniony. Mniejsza liczba alokacji skraca czas poświęcany na zbieranie danych śmieci. Umożliwia bardziej przewidywalny czas wykonywania, poprzez eliminację mechanizmów zbierania śmieci z określonych ścieżek kodu.

Typową taktyką redukcji alokacji jest zmiana krytycznych struktur danych z class typów na struct typy. Ta zmiana ma wpływ na semantyka używania tych typów. Parametry i wartości zwracane są teraz przekazywane przez wartość zamiast przez referencję. Koszt kopiowania wartości jest niewielki, jeśli typy są małe, trzy wyrazy lub mniej (biorąc pod uwagę, że jedno słowo ma naturalny rozmiar jednej liczby całkowitej). Jest to wymierne i może mieć rzeczywisty wpływ na wydajność dla większych typów. Aby zwalczać efekt kopiowania, deweloperzy mogą przekazać te typy za pomocą ref, aby uzyskać zamierzoną semantykę.

Funkcje języka C# ref umożliwiają wyrażanie żądanych semantyki typów struct bez negatywnego wpływu na ich ogólną użyteczność. Przed tymi ulepszeniami deweloperzy musieli uciekać się do unsafe konstrukcji ze wskaźnikami i nieprzetworzonymi pamięciami, aby osiągnąć ten sam wpływ na wydajność. Kompilator generuje weryfikowalny bezpieczny kod dla nowych ref powiązanych funkcji. Weryfikowalny bezpieczny kod oznacza, że kompilator wykrywa możliwe przepełnienia buforu lub uzyskuje dostęp do nieprzydzielonej lub zwolnionej pamięci. Kompilator wykrywa i zapobiega niektórym błędom.

Przekazywanie i zwracanie przez referencję

Zmienne w języku C# przechowują wartości. W struct typach wartość jest zawartością wystąpienia typu. W class typach wartość jest odwołaniem do bloku pamięci, który przechowuje wystąpienie typu. Dodanie modyfikatora ref oznacza, że zmienna przechowuje referencję do wartości. W struct typach, odwołanie wskazuje pamięć zawierającą wartość. W class typach odwołanie wskazuje na przestrzeń zawierającą odwołanie do bloku pamięci.

W języku C#parametry metod są przekazywane przez wartość, a zwracane wartości są zwracane przez wartość. Wartość argumentu jest przekazywana do metody . Wartość argumentu zwracanego to wartość zwracana.

Modyfikator ref, in, ref readonlylub out wskazuje, że argument jest przekazywany przez odwołanie. Do metody przekazywane jest odwołanie do lokalizacji magazynu. Dodanie ref do podpisu metody oznacza, że zwracana wartość jest zwracana przez odwołanie. Odwołanie do lokalizacji przechowywania jest wartością zwracaną.

Możesz również użyć przypisania ref , aby zmienna odwołyła się do innej zmiennej. Typowe przypisanie polega na skopiowaniu wartości z prawej strony do zmiennej znajdującej się po lewej stronie przypisania. Przypisanie ref kopiuje lokalizację pamięci zmiennej po prawej stronie do zmiennej po lewej stronie. Teraz ref odwołuje się do oryginalnej zmiennej:

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

Podczas przypisywania zmiennej zmieniasz jej wartość. Kiedy przypisujesz ref do zmiennej, zmieniasz to, do czego się odnosi.

Możesz pracować bezpośrednio z magazynem dla wartości, używając zmiennych ref, przekazywania przez odwołanie i przypisywania ref. Reguły zakresu wymuszane przez kompilator zapewniają bezpieczeństwo podczas bezpośredniej pracy z pamięcią.

Modyfikatory ref readonly i in wskazują, że argument powinien zostać przekazany przez odwołanie i nie można go ponownie przypisać w metodzie . Różnica polega na tym, że ref readonly metoda używa parametru jako zmiennej. Metoda może przechwytywać parametr, albo zwracać go jako odwołanie tylko do odczytu. W takich przypadkach należy użyć ref readonly modyfikatora. in W przeciwnym razie modyfikator zapewnia większą elastyczność. Nie musisz dodawać modyfikatora in do argumentu używanego w parametrze in, więc możesz bezpiecznie zaktualizować istniejące sygnatury API za pomocą modyfikatora in. Kompilator wyświetla ostrzeżenie, jeśli nie dodasz ref ani in modyfikatora do argumentu dla parametru ref readonly .

Kontekst bezpieczny ref

C# obejmuje zasady dla wyrażeń ref, aby zapewnić, że wyrażenie ref nie można uzyskać dostępu, gdy miejsce przechowywania, do którego się odnosi, jest już nieprawidłowe. Rozważmy następujący przykład:

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

Kompilator zgłasza błąd, ponieważ nie można zwrócić odwołania do zmiennej lokalnej z metody. Obiekt wywołujący nie może uzyskać dostępu do magazynu, do którym się odwołuje. Kontekst bezpieczny ref definiuje zakres, w którym ref wyrażenie jest bezpieczne do uzyskiwania dostępu do lub modyfikowania. W poniższej tabeli wymieniono konteksty bezpieczne ref dla typów zmiennych. ref nie można zadeklarować pól w elemencie class lub nie będącym typem ref struct, więc te wiersze nie są w tabeli.

Deklaracja bezpieczny kontekst odniesienia
inny niż ref — lokalny blok, w którym zadeklarowana jest zmienna lokalna
parametr inny niż ref aktualna metoda
ref, , ref readonlyin parametr metoda wywoływania
parametr out aktualna metoda
class pole metoda wywoływania
pole inne niż ref struct aktualna metoda
ref pole ref struct metoda wywoływania

Zmienną można ref zwrócić, jeśli jej kontekst bezpieczny ref jest metodą wywołującą. Jeśli jego kontekst bezpieczny ref jest bieżącą metodą lub blokiem, ref zwracanie jest niedozwolone. Poniższy fragment kodu przedstawia dwa przykłady. Dostęp do pola składowego można uzyskać z zakresu wywołującego metodę, więc kontekst bezpieczny ref pola klasy lub struktury jest metodą wywołującą. Ustalony kontekst ref dla parametru z modyfikatorami ref lub in obejmuje całą metodę. Oba mogą być ref zwracane z metody składowej:

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;
}

Uwaga / Notatka

Gdy modyfikator ref readonly lub in jest stosowany do parametru, ten parametr może być zwracany przez ref readonly, a nie przez ref.

Kompilator zapewnia, że odwołanie nie może uciec od bezpiecznego kontekstu ref. Możesz bezpiecznie użyć ref parametrów, ref return i ref zmiennych lokalnych, ponieważ kompilator wykrywa, jeśli przypadkowo napisałeś kod, w którym można uzyskać dostęp do wyrażenia ref, gdy jego zasoby pamięci są nieprawidłowe.

Bezpieczne struktury kontekstu i ref

ref struct typy wymagają większej liczby reguł, aby zapewnić ich bezpieczne wykorzystanie. Typ ref struct może zawierać ref pola. Wymaga to wprowadzenia bezpiecznego kontekstu. W przypadku większości typów metodą wywołującą jest bezpieczny kontekst. Innymi słowy, wartość, która nie jest ref struct, może być zawsze zwracana z metody.

Nieformalnie bezpieczny kontekst elementu ref struct to zakres, w którym można uzyskać dostęp do wszystkich pól ref . Innymi słowy, jest to skrzyżowanie bezpiecznego kontekstu ref wszystkich pól ref . Następująca metoda zwraca ReadOnlySpan<char> wartość do pola członkowskiego, więc jej bezpieczny kontekst jest metodą:

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

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

Z kolei poniższy kod generuje błąd, ponieważ ref field element członkowski Span<int> odwołuje się do przydzielonej tablicy stosu liczb całkowitych. Nie może uniknąć tej metody

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.
}

Ujednolicenie typów pamięci

Wprowadzenie System.Span<T> i System.Memory<T> zapewnia ujednolicony model do pracy z pamięcią. System.ReadOnlySpan<T> i System.ReadOnlyMemory<T> udostępniaj wersje tylko do odczytu na potrzeby uzyskiwania dostępu do pamięci. Wszystkie zapewniają abstrakcję nad segmentem pamięci przechowującym zbiór podobnych elementów. Różnica polega na tym, że Span<T> i ReadOnlySpan<T>ref struct typami, podczas gdy Memory<T> i ReadOnlyMemory<T>struct typami. Zakresy zawierają wartość ref field. W związku z tym wystąpienia segmentu nie mogą pozostawić bezpiecznego kontekstu. Bezpieczny kontekst elementu ref struct to kontekst referencyjny bezpieczny jego ref field. Implementacja Memory<T> i ReadOnlyMemory<T> usuwają to ograniczenie. Służysz się tymi typami do bezpośredniego dostępu do buforów pamięci.

Zwiększanie wydajności dzięki bezpieczeństwu ref

Użycie tych funkcji w celu zwiększenia wydajności obejmuje następujące zadania:

  • Unikaj alokacji: Gdy zmieniasz typ z class na struct, zmieniasz sposób jego przechowywania. Zmienne lokalne są przechowywane na stosie. Członkowie są przechowywani wewnętrznie podczas alokacji obiektu kontenera. Ta zmiana oznacza mniej alokacji i zmniejsza pracę modułu odśmiecającego elementy. Może to również zmniejszyć presję na pamięć, co spowoduje, że system usuwania śmieci będzie uruchamiany rzadziej.
  • Zachowaj semantykę odwołań: zmiana typu z class na struct zmienia semantykę przekazywania zmiennej do metody. Kod, który zmodyfikował stan parametrów, wymaga modyfikacji. Teraz, gdy parametr jest parametrem struct, metoda modyfikuje kopię oryginalnego obiektu. Można przywrócić oryginalną semantykę, przekazując ref jako parametr. Po tej zmianie metoda ponownie modyfikuje oryginał struct .
  • Unikaj kopiowania danych: kopiowanie większych struct typów może mieć wpływ na wydajność w niektórych ścieżkach kodu. Można również dodać modyfikator, ref aby przekazać większe struktury danych do metod za pomocą odwołania zamiast wartości.
  • Ogranicz modyfikacje: po struct przekazaniu typu przez odwołanie wywoływana metoda może zmodyfikować stan struktury. Modyfikator ref można zamienić na modyfikatory ref readonly lub in, aby wskazać, że argument nie można go zmodyfikować. Preferuj ref readonly , gdy metoda przechwytuje parametr lub zwraca go przez odwołanie readonly. Można również tworzyć readonly struct typy lub struct typy z readonly członkami, aby zapewnić większą kontrolę nad tym, które elementy struct można modyfikować.
  • Bezpośrednie manipulowanie pamięcią: niektóre algorytmy są najbardziej wydajne podczas traktowania struktur danych jako bloku pamięci zawierającej sekwencję elementów. Typy Span i Memory zapewniają bezpieczny dostęp do bloków pamięci.

Żadna z tych technik nie wymaga unsafe kodu. W sposób rozsądny można uzyskać charakterystykę wydajności z bezpiecznego kodu, który był wcześniej możliwy tylko przy użyciu niebezpiecznych technik. Techniki możesz wypróbować samodzielnie w samouczku dotyczącym zmniejszenia alokacji pamięci.