チュートリアル:
多くの場合、.NET アプリケーションのパフォーマンス チューニングには 2 つの手法が必要です。 まず、ヒープ割り当ての数とサイズを削減します。 次に、データをコピーする頻度を減らします。 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 オブジェクト割り当てツールを使用してアプリケーションを実行します。
Release
ビルドではなく、Debug
ビルドを使用していることを確認します。 [ デバッグ ] メニューで、 パフォーマンス プロファイラーを開きます。
.NET オブジェクト割り当て追跡オプションをオンにしますが、それ以外は何も行いません。 アプリケーションを実行して完了します。 プロファイラーは、オブジェクトの割り当てを測定し、割り当てとガベージ コレクション サイクルに関するレポートを行います。 次の図のようなグラフが表示されます。
前のグラフは、割り当てを最小限に抑えるために作業するとパフォーマンス上の利点があることを示しています。 ライブ オブジェクト グラフにノコギリのパターンが表示されます。 これは、すぐに廃棄される多数のオブジェクトが作成されることを示します。 オブジェクトデルタグラフに示すように、後で収集されます。 下向きの赤いバーは、ガベージ コレクション サイクルを示します。
次に、グラフの下にある [割り当て] タブを確認します。 次の表は、最も多く割り当てられている型を示しています。
System.Stringの種類は、最も多くの割り当てを考慮します。 最も重要なタスクは、文字列の割り当ての頻度を最小限に抑える必要があります。 このアプリケーションは、常にコンソールに多数の書式設定された出力を出力します。 このシミュレーションでは、メッセージを保持するため、次の 2 つの行 ( 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
};
}
すべての測定では、新しい SensorMeasurement
オブジェクトが割り当てられます。これは class
型です。
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}
""";
}
この型は、多数のclass
測定値が含まれているため、最初はdouble
として作成されました。 これは、ホット パスでコピーするよりも大きくなります。 ただし、その決定は多数の割り当てを意味しました。 型を class
から struct
に変更します。
class
からstruct
に変更すると、使用された元のコードnull
参照チェックがいくつか行われるため、いくつかのコンパイラ エラーが発生します。 1 つ目は、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)
他の 3 つのコンパイラ エラーはすべて、部屋で繰り返し測定を行うメソッドにあります。
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;
SensorMeasurement
が struct
ではなく class
になったため、Null 許容は "Null 許容参照型" になります。 宣言を値型に変更して、残りのコンパイラ エラーを修正できます。
SensorMeasurement measure = default;
コンパイラ エラーに対処したら、コードを調べてセマンティクスが変更されていないことを確認する必要があります。
struct
型は値渡しされるため、メソッド パラメーターに加えられた変更は、メソッドが戻った後は表示されません。
Von Bedeutung
型を class
から struct
に変更すると、プログラムのセマンティクスが変更される可能性があります。
class
型がメソッドに渡されると、メソッドで行われたすべての変更が引数に対して行われます。
struct
型がメソッドに渡され、メソッドで行われた変更が引数のコピーに対して行われる場合。 つまり、ref
からclass
に変更した引数の型でstruct
修飾子を使用するように、デザインによって引数を変更するすべてのメソッドを更新する必要があります。
SensorMeasurement
型には、状態を変更するメソッドは含まれていないため、このサンプルでは問題になりません。
readonly
構造体に SensorMeasurement
修飾子を追加することで、次のことが証明できます。
public readonly struct SensorMeasurement
コンパイラは、readonly
構造体のSensorMeasurement
の性質を適用します。 コードの検査で状態を変更するメソッドが見落とされた場合、コンパイラから通知されます。 アプリは引き続きエラーなしでビルドされるため、この種類は readonly
。 型をreadonly
からclass
に変更するときにstruct
修飾子を追加すると、struct
の状態を変更するメンバーを見つけるのに役立ちます。
コピーの作成を避ける
アプリから大量の不要な割り当てが削除されました。
SensorMeasurement
型は、テーブル内のどこにも表示されません。
現在、SensorMeasurement
構造体がパラメーターまたは戻り値として使用されるたびに、その構造体をコピーする余分な作業が行われています。
SensorMeasurement
構造体には、4 つの double、DateTime、string
が含まれています。 その構造は、参照よりも測定可能に大きくなります。
ref
型が使用される場所にin
またはSensorMeasurement
修飾子を追加してみましょう。
次の手順では、測定値を返すメソッドを見つけるか、または測定を引数として取得し、可能な場合は参照を使用します。
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
をもう一度見てみましょう。
in
パラメーターに measurement
修飾子を追加する必要があります。
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);
}
これにより、1 つのコピー操作が保存されます。
in
パラメーターは、呼び出し元によって既に作成されたコピーへの参照です。
TakeMeasurement
型の Room
メソッドを使用してコピーを保存することもできます。 このメソッドは、 ref
によって引数を渡すときにコンパイラが安全性を提供する方法を示しています。
TakeMeasurement
型の初期Room
メソッドは、Func<SensorMeasurement, bool>
の引数を受け取ります。
in
またはref
修飾子をその宣言に追加しようとすると、コンパイラによってエラーが報告されます。 ラムダ式に ref
引数を渡すことはできません。 コンパイラは、呼び出された式が参照をコピーしないことを保証できません。 ラムダ式が参照を キャプチャ する場合、参照が参照する値よりも有効期間が長くなる可能性があります。
ref セーフ コンテキストの外部でアクセスすると、メモリが破損します。
ref
の安全規則では許可されません。 詳細については、 ref 安全機能の概要を参照してください。
セマンティクスを保持する
最終的な変更セットは、ホット パスで作成されないため、このアプリケーションのパフォーマンスに大きな影響はありません。 これらの変更は、パフォーマンス チューニングで使用するその他の手法の一部を示しています。 最初の 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
オブジェクトを作成するには、複数の割り当てが必要です。 1 つは Room
自体用で、1 つは含まれている class
型のメンバーごとに 1 つです。 これらのプロパティの 2 つを class
型から struct
型 ( DebounceMeasurement
型と AverageMeasurement
型) に変換できます。 両方の型を使用して、その変換を実行してみましょう。
DebounceMeasurement
の種類をclass
からstruct
に変更します。 これにより、コンパイラ エラー CS8983: A 'struct' with field initializers must include an explicitly declared constructor
が発生します。 これを修正するには、空のパラメーターなしのコンストラクターを追加します。
public DebounceMeasurement() { }
この要件の詳細については、 構造体の言語リファレンス記事を参照してください。
Object.ToString()オーバーライドでは、構造体の値は変更されません。
readonly
修飾子をそのメソッド宣言に追加できます。
DebounceMeasurement
型は変更可能であるため、変更が破棄されるコピーに影響しないように注意する必要があります。
AddMeasurement
メソッドは、オブジェクトの状態を変更します。
Room
メソッドで、TakeMeasurements
クラスから呼び出されます。 これらの変更は、メソッドを呼び出した後も保持する必要があります。
Room.Debounce
プロパティを変更して、型の 1 つのインスタンスDebounceMeasurement
を返すことができます。
private DebounceMeasurement debounce = new();
public ref readonly DebounceMeasurement Debounce { get { return ref debounce; } }
前の例では、いくつかの変更があります。 まず、 このプロパティ は、このルームが所有するインスタンスへの読み取り専用参照を返す読み取り専用プロパティです。 これで、 Room
オブジェクトがインスタンス化されるときに初期化される宣言されたフィールドによってサポートされるようになりました。 これらの変更を行った後、 AddMeasurement
メソッドの実装を更新します。
debounce
の読み取り専用プロパティではなく、Debounce
プライベート バッキング フィールドを使用します。 そうすることで、初期化中に作成された 1 つのインスタンスで変更が行われます。
同じ手法は、 Average
プロパティでも機能します。 まず、AverageMeasurement
型をclass
からstruct
に変更し、readonly
メソッドにToString
修飾子を追加します。
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}
""";
}
次に、Room
プロパティに使用したのと同じ手法に従って、Debounce
クラスを変更します。
Average
プロパティは、平均測定値のプライベート フィールドにreadonly ref
を返します。
AddMeasurement
メソッドは、内部フィールドを変更します。
private AverageMeasurement average = new();
public ref readonly AverageMeasurement Average { get { return ref average; } }
ボックス化を回避する
パフォーマンスを向上させるための最後の変更が 1 つあります。 主なプログラムは、リスク評価を含む部屋の統計を印刷することです。
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());
プロファイラーを使用してアプリを実行し、更新されたテーブルで割り当てを確認します。
割り当てを多数削除したことで、アプリのパフォーマンスが向上しました。
アプリケーションでの ref safety の使用
これらの手法は、低レベルのパフォーマンス チューニングです。 ホット パスに適用した場合や、変更の前後に影響を測定したときに、アプリケーションのパフォーマンスが向上する可能性があります。 ほとんどの場合、従うサイクルは次のとおりです。
- 割り当ての測定: どの型が最も多く割り当てられているかを把握し、ヒープ割り当てを減らすタイミングを判断します。
-
クラスを構造体に変換する: 多くの場合、型は
class
からstruct
に変換できます。 アプリでは、ヒープの割り当てを行う代わりにスタック領域を使用します。 -
セマンティクスを保持する:
class
をstruct
に変換すると、パラメーターと戻り値のセマンティクスに影響する可能性があります。 パラメーターを変更するすべてのメソッドで、これらのパラメーターをref
修飾子でマークする必要があります。 これにより、正しいオブジェクトに変更が加えられます。 同様に、プロパティまたはメソッドの戻り値を呼び出し元が変更する必要がある場合は、その戻り値をref
修飾子でマークする必要があります。 -
コピーを回避する: 大きな構造体をパラメーターとして渡す場合は、パラメーターを
in
修飾子でマークできます。 より少ないバイト数で参照を渡し、メソッドが元の値を変更しないようにすることができます。readonly ref
して値を返して、変更できない参照を返すこともできます。
これらの手法を使用すると、コードのホット パスのパフォーマンスを向上させることができます。
.NET