새로운 C# 기능을 사용하여 메모리 할당 줄이기

Important

이 섹션에 설명된 기술은 코드의 실행 부하 과다 경로에 적용할 때 성능을 개선합니다. 실행 부하 과다 경로는 일반 작업에서 자주 반복적으로 실행되는 코드베이스의 섹션입니다. 자주 실행되지 않는 코드에 이러한 기술을 적용하면 영향이 최소화됩니다. 성능 개선을 위해 변경하기 전에 기준을 측정해야 합니다. 그런 다음 해당 기준을 분석하여 메모리 병목 현상이 발생하는 위치를 확인합니다. 진단 및 계측 섹션에서 애플리케이션 성능을 측정하는 다양한 플랫폼 간 도구에 대해 알아볼 수 있습니다. Visual Studio 설명서의 메모리 사용량 측정 자습서에서 프로파일링 세션을 실습할 수 있습니다.

메모리 사용량을 측정하고 할당을 줄일 수 있다고 판단한 후에는 이 섹션의 기술을 사용하여 할당을 줄입니다. 연속적인 변경이 있을 때마다 메모리 사용량을 다시 측정합니다. 각 변경 내용이 애플리케이션의 메모리 사용량에 긍정적인 영향을 미치는지 확인합니다.

.NET에서의 성능 작업은 종종 코드에서 할당을 제거하는 것을 의미합니다. 할당한 모든 메모리 블록은 결국 해제되어야 합니다. 할당량이 적으면 가비지 수집에 소요되는 시간이 줄어듭니다. 특정 코드 경로에서 가비지 수집을 제거하여 보다 예측 가능한 실행 시간을 허용합니다.

할당을 줄이는 일반적인 전략은 중요한 데이터 구조를 class 형식에서 struct 형식으로 변경하는 것입니다. 이 변경 내용은 해당 형식을 사용하는 의미 체계에 영향을 미칩니다. 이제 매개 변수와 반환값이 참조 대신 값으로 전달됩니다. 형식이 작고 세 단어 이하인 경우 값을 복사하는 비용은 무시할 수 있습니다(한 단어가 하나의 정수의 보통 크기인 것을 고려). 이는 측정 가능하며 더 큰 형식의 경우 실제 성능에 영향을 미칠 수 있습니다. 복사 효과를 방지하기 위해 개발자는 이러한 형식을 ref로 전달하여 의도한 의미 체계를 다시 가져올 수 있습니다.

C# ref 기능을 사용하면 전반적인 유용성에 부정적인 영향을 주지 않고 struct 형식에 대해 원하는 의미 체계를 표현할 수 있습니다. 이러한 개선 사항 이전에 개발자는 동일한 성능 영향을 얻기 위해 포인터와 원시 메모리가 포함된 unsafe 구문에 의존해야 했습니다. 컴파일러는 새로운 ref 관련 기능에 대해 검증 가능한 안전한 코드를 생성합니다. 확실히 안전한 코드는 컴파일러가 가능한 버퍼 오버런이나 할당되지 않거나 해제된 메모리에 액세스하는 것을 검색한다는 것을 의미합니다. 컴파일러는 일부 오류를 검색하고 방지합니다.

참조로 전달 및 반환

C#의 변수는 을 저장합니다. struct 형식에서 값은 해당 형식의 인스턴스 콘텐츠입니다. class 형식에서 값은 해당 형식의 인스턴스를 저장하는 메모리 블록에 대한 참조입니다. ref 한정자를 추가한다는 것은 변수가 값에 대한 참조를 저장한다는 의미입니다. struct 형식에서 참조는 값이 포함된 스토리지를 가리킵니다. class 형식에서 참조는 메모리 블록에 대한 참조를 포함하는 스토리지를 가리킵니다.

C#에서 메서드에 대한 매개 변수는 값으로 전달되고, 반환 값은 값으로 반환됩니다. 인수의 이 메서드에 전달됩니다. 반환 인수의 은 반환 값입니다.

ref, in, ref readonly 또는 out 한정자는 인수가 참조로 전달되었음을 나타냅니다. 스토리지 위치에 대한 참조가 메서드에 전달됩니다. 메서드 서명에 ref를 추가하면 반환 값이 참조로 반환된다는 의미입니다. 스토리지 위치에 대한 참조가 반환 값입니다.

또한 ref 할당을 사용하여 변수가 다른 변수를 참조하도록 할 수도 있습니다. 일반적인 할당은 오른쪽의 을 할당의 왼쪽에 있는 변수에 복사합니다. ref 할당은 오른쪽 변수의 메모리 위치를 왼쪽 변수에 복사합니다. 이제 ref는 원래 변수를 참조하세요.

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

변수를 할당하면 해당 이 변경됩니다. 변수를 ref 할당하면 변수가 참조하는 내용이 변경됩니다.

ref 변수, 참조로 전달 및 ref 할당을 사용하여 값 스토리지로 직접 작업할 수 있습니다. 컴파일러에 의해 적용되는 범위 규칙은 스토리지로 직접 작업할 때 안전을 보장합니다.

ref readonlyin 한정자는 모두 인수가 참조로 전달되어야 하며 메서드에서 다시 할당될 수 없음을 나타냅니다. 차이점은 ref readonly가 메서드가 매개 변수를 변수로 사용한다는 것을 나타냅니다. 메서드는 매개 변수를 캡처하거나 읽기 전용 참조로 매개 변수를 반환할 수 있습니다. 이러한 경우에는 ref readonly 한정자를 사용해야 합니다. 그렇지 않으면 in 한정자가 더 많은 유연성을 제공합니다. in 매개 변수의 인수에 in 한정자를 추가할 필요가 없으므로 in 한정자를 사용하여 기존 API 서명을 안전하게 업데이트할 수 있습니다. ref readonly 매개 변수의 인수에 ref 또는 in 한정자를 추가하지 않으면 컴파일러는 경고를 표시합니다.

ref safe 컨텍스트

C#에는 참조하는 스토리지가 더 이상 유효하지 않은 경우 ref 식에 액세스할 수 없도록 하는 ref 식에 대한 규칙이 포함되어 있습니다. 다음 예제를 참조하세요.

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

메서드에서 지역 변수에 대한 참조를 반환할 수 없기 때문에 컴파일러는 오류를 보고합니다. 호출자는 참조되는 스토리지에 액세스할 수 없습니다. ref safe 컨텍스트ref 식이 액세스하거나 수정하기에 safe 범위를 정의합니다. 다음 표에는 변수 형식에 대한 ref safe 컨텍스트가 나열되어 있습니다. ref 필드는 class 또는 ref가 아닌 struct에서 선언될 수 없으므로 해당 행은 테이블에 없습니다.

선언 ref safe 컨텍스트
ref가 아닌 로컬 local이 선언된 블록
ref가 아닌 매개 변수 현재 메서드
ref, ref readonly, in 매개 변수 호출 메서드
out 매개 변수 현재 메서드
class 필드 호출 메서드
ref가 아닌 struct 필드 현재 메서드
ref structref 필드 호출 메서드

ref safe 컨텍스트가 호출 메서드인 경우 변수는 ref 반환될 수 있습니다. ref safe 컨텍스트가 현재 메서드 또는 블록인 경우 ref 반환이 허용되지 않습니다. 다음 코드 조각은 두 가지 예를 보여 줍니다. 메서드를 호출하는 범위에서 멤버 필드에 액세스할 수 있으므로 클래스 또는 구조체 필드의 ref safe 컨텍스트가 호출 메서드입니다. ref 또는 in 한정자가 있는 매개 변수에 대한 ref safe 컨텍스트는 전체 메서드입니다. 둘 다 멤버 메서드에서 ref 반환될 수 있습니다.

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

참고 항목

ref readonly 또는 in 한정자가 매개 변수에 적용되면 해당 매개 변수는 ref가 아닌 ref readonly에 의해 반환될 수 있습니다.

컴파일러는 참조가 ref safe 컨텍스트를 벗어날 수 없도록 보장합니다. 스토리지가 유효하지 않을 때 ref 식에 액세스할 수 있는 코드를 실수로 작성했는지 컴파일러가 검색하므로 ref 매개 변수, ref returnref 지역 변수를 안전하게 사용할 수 있습니다.

safe 컨텍스트 및 ref 구조체

ref struct 형식을 안전하게 사용하려면 더 많은 규칙이 필요합니다. ref struct 형식에는 ref 필드가 포함될 수 있습니다. 이를 위해서는 safe 컨텍스트의 도입이 필요합니다. 대부분의 형식에서 safe 컨텍스트는 호출 메서드입니다. 즉, ref struct가 아닌 값은 항상 메서드에서 반환될 수 있습니다.

비공식적으로 ref structsafe 컨텍스트는 모든 ref 필드에 액세스할 수 있는 범위입니다. 즉, 모든 ref 필드의 ref safe 컨텍스트가 교차하는 지점입니다. 다음 메서드는 멤버 필드에 ReadOnlySpan<char>를 반환하므로 해당 safe 컨텍스트가 메서드입니다.

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

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

반면에 다음 코드는 Span<int>ref field 멤버가 스택에 할당된 정수 배열을 참조하기 때문에 오류를 발생시킵니다. 메서드를 벗어날 수 없습니다.

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

메모리 형식 통합

System.Span<T>System.Memory<T>의 도입으로 메모리 작업을 위한 통합 모델이 제공됩니다. System.ReadOnlySpan<T>System.ReadOnlyMemory<T>는 메모리 액세스를 위한 읽기 전용 버전을 제공합니다. 이들은 모두 유사한 요소의 배열을 저장하는 메모리 블록에 대한 추상화를 제공합니다. 차이점은 Span<T>ReadOnlySpan<T>ref struct 형식이고 Memory<T>ReadOnlyMemory<T>struct 형식이라는 것입니다. 범위에는 ref field가 포함되어 있습니다. 따라서 범위의 인스턴스는 safe 컨텍스트를 벗어날 수 없습니다. ref structsafe 컨텍스트ref fieldref safe 컨텍스트입니다. Memory<T>ReadOnlyMemory<T>를 구현하면 이 제한이 제거됩니다. 이러한 형식을 사용하여 메모리 버퍼에 직접 액세스합니다.

ref safety로 성능 개선

성능을 개선시키기 위해 이러한 기능을 사용하는 작업에는 다음이 포함됩니다.

  • 할당 방지: 형식을 class에서 struct로 변경하면 저장 방법이 변경됩니다. 지역 변수는 스택에 저장됩니다. 멤버는 컨테이너 개체가 할당될 때 인라인으로 저장됩니다. 이 변경은 할당 횟수가 줄어들고 가비지 수집기의 작업이 줄어드는 것을 의미합니다. 또한 메모리 압력을 줄여 가비지 수집기 실행 빈도를 줄일 수도 있습니다.
  • 참조 의미 체계 유지: 형식을 class에서 struct로 변경하면 변수를 메서드에 전달하는 의미 체계가 변경됩니다. 매개 변수의 상태를 수정한 코드는 수정이 필요합니다. 이제 매개 변수가 struct이므로 메서드가 원본 개체의 복사본을 수정합니다. 해당 매개 변수를 ref 매개 변수로 전달하여 원래 의미 체계를 복원할 수 있습니다. 변경 후 메서드는 원래 struct를 다시 수정합니다.
  • 데이터 복사 방지: 더 큰 struct 형식을 복사하면 일부 코드 경로의 성능에 영향을 미칠 수 있습니다. 또한 ref 한정자를 추가하여 값 대신 참조로 더 큰 데이터 구조를 메서드에 전달할 수도 있습니다.
  • 수정 제한: struct 형식이 참조로 전달되면 호출된 메서드가 구조체의 상태를 수정할 수 있습니다. ref 한정자를 ref readonly 또는 in 한정자로 바꿔 인수를 수정할 수 없음을 나타낼 수 있습니다. 메서드가 매개 변수를 캡처하거나 읽기 전용 참조로 반환하는 경우 ref readonly를 선호합니다. readonly 멤버로 readonly struct 형식 또는 struct 형식을 만들어 수정할 수 있는 struct 멤버를 더 효과적으로 제어할 수도 있습니다.
  • 메모리 직접 조작: 일부 알고리즘은 데이터 구조를 일련의 요소가 포함된 메모리 블록으로 처리할 때 가장 효율적입니다. SpanMemory 형식은 메모리 블록에 대한 안전한 액세스를 제공합니다.

이러한 기술에는 unsafe 코드가 필요하지 않습니다. 현명하게 사용하면 이전에는 안전하지 않은 기술을 통해서만 가능했던 안전한 코드로부터 성능 특성을 가져올 수 있습니다. 메모리 할당 줄이기에 대한 자습서에서 기술을 직접 시도해 볼 수 있습니다.