다음을 통해 공유


자습서: ref 안전성으로 메모리 할당 줄이기

.NET 애플리케이션의 성능 튜닝에는 두 가지 기술이 필요한 경우가 많습니다. 먼저, 힙 할당의 수와 크기를 줄입니다. 둘째, 데이터가 복사되는 빈도를 줄입니다. Visual Studio는 애플리케이션이 메모리를 사용하는 방식을 분석하는 데 도움이 되는 훌륭한 도구를 제공합니다. 앱에서 불필요한 할당을 수행하는 위치를 확인한 후에는 해당 할당을 최소화하도록 변경합니다. class 형식을 struct 형식으로 변환합니다. 의미 체계를 유지하고 추가 복사를 최소화하려면 ref 안전 기능을 사용합니다.

이 자습서를 최대한 활용하려면 Visual Studio 17.5를 사용합니다. 메모리 사용량을 분석하는 데 사용되는 .NET 개체 할당 도구는 Visual Studio의 일부입니다. Visual Studio Code와 명령줄을 사용하여 애플리케이션을 실행하고 모든 변경 작업을 수행할 수 있습니다. 그러나 변경 내용에 대한 분석 결과를 볼 수는 없습니다.

사용하게 될 애플리케이션은 침입자가 귀중품을 가지고 비밀 갤러리에 들어왔는지 확인하기 위해 여러 센서를 모니터링하는 IoT 애플리케이션의 시뮬레이션입니다. IoT 센서는 공기 중 산소(O2)와 이산화탄소(CO2)의 혼합을 측정하는 데이터를 지속적으로 전송합니다. 또한 온도와 상대 습도도 보고합니다. 이러한 각 값은 항상 약간씩 변동합니다. 그러나 사람이 방에 들어가면 변화가 조금 더 심해지고 항상 같은 방향으로 나타납니다. 즉, 산소는 감소하고 이산화탄소는 증가하며 온도는 증가하고 상대 습도도 증가합니다. 센서가 결합되어 증가를 표시하면 침입자 경보가 트리거됩니다.

이 자습서에서는 애플리케이션을 실행하고 메모리 할당을 측정한 다음 할당 수를 줄여 성능을 개선합니다. 소스 코드는 샘플 브라우저에서 사용할 수 있습니다.

시작 애플리케이션 살펴보기

애플리케이션을 다운로드하고 시작 샘플을 실행합니다. 시작 애플리케이션은 올바르게 작동하지만 각 측정 주기마다 많은 작은 개체를 할당하기 때문에 시간이 지남에 따라 실행되면서 성능이 서서히 저하됩니다.

Press <return> to start simulation

Debounced measurements:
    Temp:      67.332
    Humidity:  41.077%
    Oxygen:    21.097%
    CO2 (ppm): 404.906
Average measurements:
    Temp:      67.332
    Humidity:  41.077%
    Oxygen:    21.097%
    CO2 (ppm): 404.906

Debounced measurements:
    Temp:      67.349
    Humidity:  46.605%
    Oxygen:    20.998%
    CO2 (ppm): 408.707
Average measurements:
    Temp:      67.349
    Humidity:  46.605%
    Oxygen:    20.998%
    CO2 (ppm): 408.707

많은 행이 제거되었습니다.

Debounced measurements:
    Temp:      67.597
    Humidity:  46.543%
    Oxygen:    19.021%
    CO2 (ppm): 429.149
Average measurements:
    Temp:      67.568
    Humidity:  45.684%
    Oxygen:    19.631%
    CO2 (ppm): 423.498
Current intruders: 3
Calculated intruder risk: High

Debounced measurements:
    Temp:      67.602
    Humidity:  46.835%
    Oxygen:    19.003%
    CO2 (ppm): 429.393
Average measurements:
    Temp:      67.568
    Humidity:  45.684%
    Oxygen:    19.631%
    CO2 (ppm): 423.498
Current intruders: 3
Calculated intruder risk: High

코드를 탐색하여 애플리케이션 작동 방식을 알아볼 수 있습니다. 메인 프로그램은 시뮬레이션을 실행합니다. <Enter>를 누르면 방이 만들어지고 일부 초기 기준 데이터가 수집됩니다.

Console.WriteLine("Press <return> to start simulation");
Console.ReadLine();
var room = new Room("gallery");
var r = new Random();

int counter = 0;

room.TakeMeasurements(
    m =>
    {
        Console.WriteLine(room.Debounce);
        Console.WriteLine(room.Average);
        Console.WriteLine();
        counter++;
        return counter < 20000;
    });

기준 데이터가 설정되면 방에서 시뮬레이션을 실행합니다. 여기서 난수 생성기는 침입자가 방에 들어왔는지 확인합니다.

counter = 0;
room.TakeMeasurements(
    m =>
    {
        Console.WriteLine(room.Debounce);
        Console.WriteLine(room.Average);
        room.Intruders += (room.Intruders, r.Next(5)) switch
        {
            ( > 0, 0) => -1,
            ( < 3, 1) => 1,
            _ => 0
        };

        Console.WriteLine($"Current intruders: {room.Intruders}");
        Console.WriteLine($"Calculated intruder risk: {room.RiskStatus}");
        Console.WriteLine();
        counter++;
        return counter < 200000;
    });

다른 형식에는 측정값, 최근 50개 측정값의 평균인 디바운싱된 측정값 및 수행된 모든 측정값의 평균이 포함됩니다.

그런 다음 .NET 개체 할당 도구를 사용하여 애플리케이션을 실행합니다. Debug 빌드가 아닌 Release 빌드를 사용하고 있는지 확인합니다. 디버그 메뉴에서 성능 프로파일러를 엽니다. .NET 개체 할당 추적 옵션을 확인하고 다른 옵션은 확인하지 않습니다. 애플리케이션을 완료할 때까지 실행합니다. 프로파일러는 개체 할당을 측정하고 할당 및 가비지 수집 주기에 대해 보고합니다. 다음 이미지와 유사한 그래프가 표시됩니다.

Allocation graph for running the intruder alert app before any optimizations.

이전 그래프에서는 할당을 최소화하는 작업이 성능상의 이점을 제공한다는 것을 보여 줍니다. 실시간 개체 그래프에 톱니 모양 패턴이 표시됩니다. 이는 빠르게 쓰레기가 되는 수많은 개체가 만들어진다는 것을 알려 줍니다. 개체 델타 그래프에 표시된 대로 나중에 수집됩니다. 아래쪽 빨간색 막대는 가비지 수집 주기를 나타냅니다.

다음으로 그래프 아래에 있는 할당 탭을 살펴봅니다. 다음 표는 가장 많이 할당된 형식을 보여 줍니다.

Chart that shows which types are allocated most frequently.

System.String 형식은 가장 많은 할당을 설명합니다. 가장 중요한 작업은 문자열 할당 빈도를 최소화하는 것입니다. 이 애플리케이션은 다양한 형식의 출력을 콘솔에 지속적으로 인쇄합니다. 이 시뮬레이션에서는 메시지를 유지하려고 하므로 다음 두 행인 SensorMeasurement 형식과 IntruderRisk 형식에 집중할 예정입니다.

SensorMeasurement 라인을 두 번 클릭합니다. 모든 할당이 static 메서드 SensorMeasurement.TakeMeasurement에서 발생하는 것을 볼 수 있습니다. 다음 코드 조각에서 메서드를 볼 수 있습니다.

public static SensorMeasurement TakeMeasurement(string room, int intruders)
{
    return new SensorMeasurement
    {
        CO2 = (CO2Concentration + intruders * 10) + (20 * generator.NextDouble() - 10.0),
        O2 = (O2Concentration - intruders * 0.01) + (0.005 * generator.NextDouble() - 0.0025),
        Temperature = (TemperatureSetting + intruders * 0.05) + (0.5 * generator.NextDouble() - 0.25),
        Humidity = (HumiditySetting + intruders * 0.005) + (0.20 * generator.NextDouble() - 0.10),
        Room = room,
        TimeRecorded = DateTime.Now
    };
}

모든 측정은 class 형식인 새로운 SensorMeasurement 개체를 할당합니다. 모든 SensorMeasurement가 만들어지면 힙이 할당됩니다.

클래스를 구조체로 변경

다음 코드는 SensorMeasurement의 초기 선언을 보여 줍니다.

public class SensorMeasurement
{
    private static readonly Random generator = new Random();

    public static SensorMeasurement TakeMeasurement(string room, int intruders)
    {
        return new SensorMeasurement
        {
            CO2 = (CO2Concentration + intruders * 10) + (20 * generator.NextDouble() - 10.0),
            O2 = (O2Concentration - intruders * 0.01) + (0.005 * generator.NextDouble() - 0.0025),
            Temperature = (TemperatureSetting + intruders * 0.05) + (0.5 * generator.NextDouble() - 0.25),
            Humidity = (HumiditySetting + intruders * 0.005) + (0.20 * generator.NextDouble() - 0.10),
            Room = room,
            TimeRecorded = DateTime.Now
        };
    }

    private const double CO2Concentration = 409.8; // increases with people.
    private const double O2Concentration = 0.2100; // decreases
    private const double TemperatureSetting = 67.5; // increases
    private const double HumiditySetting = 0.4500; // increases

    public required double CO2 { get; init; }
    public required double O2 { get; init; }
    public required double Temperature { get; init; }
    public required double Humidity { get; init; }
    public required string Room { get; init; }
    public required DateTime TimeRecorded { get; init; }

    public override string ToString() => $"""
            Room: {Room} at {TimeRecorded}:
                Temp:      {Temperature:F3}
                Humidity:  {Humidity:P3}
                Oxygen:    {O2:P3}
                CO2 (ppm): {CO2:F3}
            """;
}

이 형식은 수많은 double 측정값을 포함하므로 원래 class로 만들어졌습니다. 실행 부하 과다 경로에 복사하려는 것보다 큽니다. 그러나 그 결정은 많은 할당을 의미했습니다. 형식을 class에서 struct로 변경합니다.

class에서 struct로 변경하면 원본 코드가 몇 군데에서 null 참조 확인을 사용했기 때문에 몇 가지 컴파일러 오류가 발생합니다. 첫 번째는 DebounceMeasurement 클래스의 AddMeasurement 메서드에 있습니다.

public void AddMeasurement(SensorMeasurement datum)
{
    int index = totalMeasurements % debounceSize;
    recentMeasurements[index] = datum;
    totalMeasurements++;
    double sumCO2 = 0;
    double sumO2 = 0;
    double sumTemp = 0;
    double sumHumidity = 0;
    for (int i = 0; i < debounceSize; i++)
    {
        if (recentMeasurements[i] is not null)
        {
            sumCO2 += recentMeasurements[i].CO2;
            sumO2+= recentMeasurements[i].O2;
            sumTemp+= recentMeasurements[i].Temperature;
            sumHumidity += recentMeasurements[i].Humidity;
        }
    }
    O2 = sumO2 / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
    CO2 = sumCO2 / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
    Temperature = sumTemp / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
    Humidity = sumHumidity / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
}

DebounceMeasurement 형식에는 50개의 측정값 배열이 포함되어 있습니다. 센서 판독값은 최근 50회 측정의 평균으로 보고됩니다. 그러면 판독값의 노이즈가 줄어듭니다. 50개 전체를 판독하기 전의 값은 null입니다. 코드는 시스템 시작 시 올바른 평균을 보고하기 위해 null 참조를 확인합니다. SensorMeasurement 형식을 구조체로 변경한 후에는 다른 테스트를 사용해야 합니다. SensorMeasurement 형식에는 방 식별자에 대한 string이 포함되어 있으므로 해당 테스트를 대신 사용할 수 있습니다.

if (recentMeasurements[i].Room is not null)

다른 세 가지 컴파일러 오류는 모두 방에서 반복적으로 측정하는 방법에 있습니다.

public void TakeMeasurements(Func<SensorMeasurement, bool> MeasurementHandler)
{
    SensorMeasurement? measure = default;
    do {
        measure = SensorMeasurement.TakeMeasurement(Name, Intruders);
        Average.AddMeasurement(measure);
        Debounce.AddMeasurement(measure);
    } while (MeasurementHandler(measure));
}

시작 메서드에서 SensorMeasurement의 지역 변수는 null 허용 참조입니다.

SensorMeasurement? measure = default;

이제 SensorMeasurementclass 대신 struct이므로 null 허용 항목은 null 허용 값 형식입니다. 선언을 값 형식으로 변경하여 나머지 컴파일러 오류를 수정할 수 있습니다.

SensorMeasurement measure = default;

이제 컴파일러 오류가 해결되었으므로 코드를 검사하여 의미 체계가 변경되지 않았는지 확인해야 합니다. struct 형식은 값으로 전달되므로 메서드 매개 변수에 대한 수정 사항은 메서드가 반환된 후에 표시되지 않습니다.

Important

형식을 class에서 struct로 변경하면 프로그램의 의미 체계가 변경될 수 있습니다. class 형식이 메서드에 전달되면 메서드에서 발생한 모든 변형이 인수에 적용됩니다. struct 형식이 메서드에 전달되고 메서드에서 발생한 변형이 인수의 복사본에 만들어지는 경우. 즉, 설계상 인수를 수정하는 모든 메서드는 class에서 struct로 변경한 모든 인수 형식에 대해 ref 한정자를 사용하도록 업데이트되어야 함을 의미합니다.

SensorMeasurement 형식에는 상태를 변경하는 메서드가 포함되어 있지 않으므로 이 샘플에서는 문제가 되지 않습니다. SensorMeasurement 구조체에 readonly 한정자를 추가하여 이를 증명할 수 있습니다.

public readonly struct SensorMeasurement

컴파일러는 SensorMeasurement 구조체의 readonly 특성을 적용합니다. 코드 검사에서 상태를 수정하는 일부 메서드가 누락된 경우 컴파일러가 이를 알려 줍니다. 앱은 여전히 오류 없이 빌드되므로 이 형식은 readonly입니다. 형식을 class에서 struct로 변경할 때 readonly 한정자를 추가하면 struct의 상태를 수정하는 멤버를 찾는 데 도움이 될 수 있습니다.

복사본 만들기 방지

앱에서 불필요한 할당을 많이 제거했습니다. SensorMeasurement 형식은 테이블 어디에도 표시되지 않습니다.

이제 매개 변수나 반환 값으로 사용될 때마다 SensorMeasurement 구조를 복사하는 추가 작업을 수행하고 있습니다. SensorMeasurement 구조체에는 4개의 double, 즉 DateTimestring이 포함되어 있습니다. 해당 구조는 참조보다 측정 가능하게 더 큽니다. SensorMeasurement 형식이 사용되는 위치에 ref 또는 in 한정자를 추가해 보겠습니다.

다음 단계는 측정값을 반환하거나 측정값을 인수로 사용하고 가능한 경우 참조를 사용하는 메서드를 찾는 것입니다. SensorMeasurement 구조체에서 시작합니다. 정적 TakeMeasurement 메서드는 새 SensorMeasurement를 만들고 반환합니다.

public static SensorMeasurement TakeMeasurement(string room, int intruders)
{
    return new SensorMeasurement
    {
        CO2 = (CO2Concentration + intruders * 10) + (20 * generator.NextDouble() - 10.0),
        O2 = (O2Concentration - intruders * 0.01) + (0.005 * generator.NextDouble() - 0.0025),
        Temperature = (TemperatureSetting + intruders * 0.05) + (0.5 * generator.NextDouble() - 0.25),
        Humidity = (HumiditySetting + intruders * 0.005) + (0.20 * generator.NextDouble() - 0.10),
        Room = room,
        TimeRecorded = DateTime.Now
    };
}

이를 그대로 두고 값으로 반환할 예정입니다. ref로 반환하려고 하면 컴파일러 오류가 발생합니다. 메서드에서 로컬로 만들어진 새 구조에 ref를 반환할 수 없습니다. 변경이 불가능한 구조체의 디자인은 생성 시 측정 값만 설정할 수 있음을 의미합니다. 이 메서드는 새로운 측정 구조체를 만들어야 합니다.

DebounceMeasurement.AddMeasurement를 다시 살펴보겠습니다. measurement 매개 변수에 in 한정자를 추가해야 합니다.

public void AddMeasurement(in SensorMeasurement datum)
{
    int index = totalMeasurements % debounceSize;
    recentMeasurements[index] = datum;
    totalMeasurements++;
    double sumCO2 = 0;
    double sumO2 = 0;
    double sumTemp = 0;
    double sumHumidity = 0;
    for (int i = 0; i < debounceSize; i++)
    {
        if (recentMeasurements[i].Room is not null)
        {
            sumCO2 += recentMeasurements[i].CO2;
            sumO2+= recentMeasurements[i].O2;
            sumTemp+= recentMeasurements[i].Temperature;
            sumHumidity += recentMeasurements[i].Humidity;
        }
    }
    O2 = sumO2 / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
    CO2 = sumCO2 / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
    Temperature = sumTemp / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
    Humidity = sumHumidity / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
}

그러면 하나의 복사 작업이 절약됩니다. in 매개 변수는 호출자가 이미 만든 복사본에 대한 참조입니다. Room 형식의 TakeMeasurement 메서드를 사용하여 복사본을 저장할 수도 있습니다. 이 메서드는 ref로 인수를 전달할 때 컴파일러가 어떻게 안전을 제공하는지 보여 줍니다. Room 형식의 초기 TakeMeasurement 메서드는 Func<SensorMeasurement, bool> 인수를 사용합니다. 해당 선언에 in 또는 ref 한정자를 추가하려고 하면 컴파일러가 오류를 보고합니다. 람다 식에 ref 인수를 전달할 수 없습니다. 컴파일러는 호출된 식이 참조를 복사하지 않는다고 보장할 수 없습니다. 람다 식이 참조를 캡처하는 경우 참조는 참조하는 값보다 수명이 길어질 수 있습니다. ref safe 컨텍스트 외부에서 액세스하면 메모리가 손상될 수 있습니다. ref safe 규칙에서는 이를 허용하지 않습니다. ref safety 기능 개요에서 자세히 알아볼 수 있습니다.

의미 체계 보존

형식이 실행 부하 과다 경로에 만들어지지 않기 때문에 최종 변경 내용 집합은 이 애플리케이션의 성능에 큰 영향을 미치지 않습니다. 이러한 변경 내용은 성능 튜닝에 사용할 몇 가지 다른 기술을 보여 줍니다. 초기 Room 클래스를 살펴보겠습니다.

public class Room
{
    public AverageMeasurement Average { get; } = new ();
    public DebounceMeasurement Debounce { get; } = new ();
    public string Name { get; }

    public IntruderRisk RiskStatus
    {
        get
        {
            var CO2Variance = (Debounce.CO2 - Average.CO2) > 10.0 / 4;
            var O2Variance = (Average.O2 - Debounce.O2) > 0.005 / 4.0;
            var TempVariance = (Debounce.Temperature - Average.Temperature) > 0.05 / 4.0;
            var HumidityVariance = (Debounce.Humidity - Average.Humidity) > 0.20 / 4;
            IntruderRisk risk = IntruderRisk.None;
            if (CO2Variance) { risk++; }
            if (O2Variance) { risk++; }
            if (TempVariance) { risk++; }
            if (HumidityVariance) { risk++; }
            return risk;
        }
    }

    public int Intruders { get; set; }

    
    public Room(string name)
    {
        Name = name;
    }

    public void TakeMeasurements(Func<SensorMeasurement, bool> MeasurementHandler)
    {
        SensorMeasurement? measure = default;
        do {
            measure = SensorMeasurement.TakeMeasurement(Name, Intruders);
            Average.AddMeasurement(measure);
            Debounce.AddMeasurement(measure);
        } while (MeasurementHandler(measure));
    }
}

이 형식에는 여러 속성이 포함되어 있습니다. 일부는 class 형식입니다. Room 개체 만들기에는 여러 할당이 포함됩니다. 하나는 Room 자체에 대한 것이고 다른 하나는 포함된 class 형식의 각 멤버에 대한 것입니다. 이러한 속성 중 두 개(DebounceMeasurementAverageMeasurement 형식)를 class 형식에서 struct 형식으로 변환할 수 있습니다. 두 가지 형식을 모두 사용하여 해당 변환을 살펴보겠습니다.

DebounceMeasurement 형식을 class에서 struct로 변경합니다. 이로 인해 컴파일러 오류 CS8983: A 'struct' with field initializers must include an explicitly declared constructor가 발생합니다. 매개 변수가 없는 빈 생성자를 추가하여 이 문제를 해결할 수 있습니다.

public DebounceMeasurement() { }

구조체에 대한 언어 참조 문서에서 이 요구 사항에 대해 자세히 알아볼 수 있습니다.

Object.ToString() 재정의는 구조체 값을 수정하지 않습니다. 해당 메서드 선언에 readonly 한정자를 추가할 수 있습니다. DebounceMeasurement 형식은 변경 가능하므로 수정 사항이 삭제되는 복사본에 영향을 미치지 않도록 주의해야 합니다. AddMeasurement 메서드는 개체의 상태를 수정합니다. TakeMeasurements 메서드의 Room 클래스에서 호출됩니다. 메서드를 호출한 후에도 이러한 변경 내용을 유지하려고 합니다. Room.Debounce 속성을 변경하여 DebounceMeasurement 형식의 단일 인스턴스에 대한 참조를 반환할 수 있습니다.

private DebounceMeasurement debounce = new();
public ref readonly DebounceMeasurement Debounce { get { return ref debounce; } }

이전 예에는 몇 가지 변경 내용이 있습니다. 첫째, 속성은 이 방이 소유한 인스턴스에 대한 읽기 전용 참조를 반환하는 읽기 전용 속성입니다. 이제 Room 개체가 인스턴스화될 때 초기화되는 선언된 필드에 의해 지원됩니다. 이렇게 변경한 후에는 AddMeasurement 메서드 구현을 업데이트하게 됩니다. 읽기 전용 속성 Debounce가 아닌 프라이빗 지원 필드 debounce를 사용합니다. 이렇게 하면 초기화 중에 만들어진 단일 인스턴스에 변경 내용이 적용됩니다.

동일한 기술이 Average 속성에도 적용됩니다. 먼저, AverageMeasurement 형식을 class에서 struct로 수정하고 ToString 메서드에 readonly 한정자를 추가합니다.

namespace IntruderAlert;

public struct AverageMeasurement
{
    private double sumCO2 = 0;
    private double sumO2 = 0;
    private double sumTemperature = 0;
    private double sumHumidity = 0;
    private int totalMeasurements = 0;

    public AverageMeasurement() { }

    public readonly double CO2 => sumCO2 / totalMeasurements;
    public readonly double O2 => sumO2 / totalMeasurements;
    public readonly double Temperature => sumTemperature / totalMeasurements;
    public readonly double Humidity => sumHumidity / totalMeasurements;

    public void AddMeasurement(in SensorMeasurement datum)
    {
        totalMeasurements++;
        sumCO2 += datum.CO2;
        sumO2 += datum.O2;
        sumTemperature += datum.Temperature;
        sumHumidity+= datum.Humidity;
    }

    public readonly override string ToString() => $"""
        Average measurements:
            Temp:      {Temperature:F3}
            Humidity:  {Humidity:P3}
            Oxygen:    {O2:P3}
            CO2 (ppm): {CO2:F3}
        """;
}

그런 다음 Debounce 속성에 사용한 것과 동일한 기술에 따라 Room 클래스를 수정합니다. Average 속성은 평균 측정을 위해 프라이빗 필드에 readonly ref를 반환합니다. AddMeasurement 메서드는 내부 필드를 수정합니다.

private AverageMeasurement average = new();
public  ref readonly AverageMeasurement Average { get { return ref average; } }

boxing 방지

성능을 개선시키기 위한 마지막 변경 내용이 하나 있습니다. 주요 프로그램은 위험 평가를 포함하여 방에 대한 통계를 인쇄합니다.

Console.WriteLine($"Current intruders: {room.Intruders}");
Console.WriteLine($"Calculated intruder risk: {room.RiskStatus}");

생성된 ToString에 대한 호출은 열거형 값을 상자에 넣습니다. 예상 위험 값에 따라 문자열 형식을 지정하는 Room 클래스에 재정의를 작성하면 이를 방지할 수 있습니다.

public override string ToString() =>
    $"Calculated intruder risk: {RiskStatus switch
    {
        IntruderRisk.None => "None",
        IntruderRisk.Low => "Low",
        IntruderRisk.Medium => "Medium",
        IntruderRisk.High => "High",
        IntruderRisk.Extreme => "Extreme",
        _ => "Error!"
    }}, Current intruders: {Intruders.ToString()}";

그런 다음 기본 프로그램의 코드를 수정하여 이 새 ToString 메서드를 호출합니다.

Console.WriteLine(room.ToString());

프로파일러를 사용하여 앱을 실행하고 업데이트된 할당 테이블을 살펴봅니다.

Allocation graph for running the intruder alert app after modifications.

수많은 할당을 제거하고 앱 성능을 향상시켰습니다.

애플리케이션에 ref safety 사용

이러한 기술은 낮은 수준의 성능 튜닝입니다. 실행 부하 과다 경로에 적용하고 변경 전후의 영향을 측정하면 애플리케이션의 성능이 향상될 수 있습니다. 대부분의 경우 따르는 주기는 다음과 같습니다.

  • 할당 측정: 가장 많이 할당되는 형식과 힙 할당을 줄일 수 있는 시기를 결정합니다.
  • 클래스를 구조체로 변환: 형식을 class에서 struct로 변환할 수 있는 경우가 많습니다. 앱은 힙을 할당하는 대신 스택 공간을 사용합니다.
  • 의미 체계 보존: classstruct로 변환하면 매개 변수 및 반환 값의 의미 체계에 영향을 미칠 수 있습니다. 매개 변수를 수정하는 모든 메서드는 이제 해당 매개 변수를 ref 한정자로 표시해야 합니다. 이를 통해 올바른 개체에 대한 수정이 이루어집니다. 마찬가지로 호출자가 속성이나 메서드 반환 값을 수정해야 하는 경우 해당 반환은 ref 한정자로 표시되어야 합니다.
  • 복사 방지: 큰 구조체를 매개 변수로 전달하는 경우 in 한정자로 매개 변수를 표시할 수 있습니다. 더 적은 바이트로 참조를 전달할 수 있으며 메서드가 원래 값을 수정하지 않도록 할 수 있습니다. 또한 readonly ref로 값을 반환하여 수정할 수 없는 참조를 반환할 수도 있습니다.

이러한 기술을 사용하면 코드의 실행 부하 과다 경로 성능을 개선시킬 수 있습니다.