C#の処理時間を正確に計測する方法|Stopwatchの使い方と高速化のコツ
はじめに
C#でアプリケーションやツールを開発していると、「この処理はどのくらい時間がかかっているのか」「どのコードが遅い原因なのか」を確認したくなる場面があります。処理時間を感覚だけで判断すると、実際には遅くない部分を改善しようとしたり、本当にボトルネックになっている処理を見逃したりすることがあります。
C#で処理時間を計測する場合、基本的にはStopwatchクラスを使うのが一般的です。Stopwatchは高精度な経過時間の計測に向いており、ミリ秒単位の処理時間や、さらに細かい単位の経過時間を確認できます。
この記事では、C#で処理時間を正確に計測する方法を、Stopwatchの基本的な使い方から、DateTimeとの違い、実用的なサンプルコード、計測時の注意点、処理を高速化するコツまでわかりやすく解説します。
1. C#の処理時間を計測したい人の検索意図
1-1. コードのどこが遅いのかを数値で確認したい
C#の処理時間を調べたい人の多くは、「アプリケーションが重い」「画面表示が遅い」「ファイル処理に時間がかかる」といった問題を抱えています。
ただし、処理が遅いと感じても、原因がデータベースアクセスなのか、ファイル読み込みなのか、ループ処理なのか、LINQなのかは計測してみないとわかりません。
処理時間を数値で確認すれば、改善すべき箇所を客観的に判断できます。
C#var sw = Stopwatch.StartNew();
HeavyProcess();
sw.Stop();
Console.WriteLine($"処理時間: {sw.ElapsedMilliseconds} ms");
このように計測すれば、対象の処理に何ミリ秒かかったのかを確認できます。
1-2. Stopwatchを使って正確に処理時間を測りたい
C#ではDateTime.Nowを使って処理時間を測ることもできますが、正確な経過時間を測りたい場合はStopwatchを使うのが基本です。
Stopwatchは経過時間の計測を目的として用意されているクラスです。処理の開始時点でスタートし、終了時点でストップすることで、その間にかかった時間を取得できます。
特に、短い処理や高速な処理を測定したい場合、Stopwatchの方が適しています。
1-3. DateTimeとの違いや使い分けを知りたい
DateTime.Nowは現在時刻を取得するためのものです。そのため、処理時間の計測にも使えますが、本来の目的は「今が何時か」を扱うことです。
一方、Stopwatchは「開始から終了までにどれくらい時間が経過したか」を測るためのものです。
そのため、処理時間の計測にはStopwatch、ログに実行日時を残したい場合はDateTimeというように使い分けるのがよいでしょう。
1-4. ミリ秒・秒・Ticksなど結果の見方を知りたい
Stopwatchでは、計測結果をさまざまな形式で取得できます。
代表的なものは次の通りです。
C#Console.WriteLine(sw.Elapsed);
Console.WriteLine(sw.ElapsedMilliseconds);
Console.WriteLine(sw.ElapsedTicks);
Console.WriteLine(sw.Elapsed.TotalSeconds);
Console.WriteLine(sw.Elapsed.TotalMilliseconds);
ElapsedMillisecondsは整数のミリ秒、Elapsed.TotalMillisecondsは小数を含むミリ秒です。細かい違いを理解しておくと、計測結果を正しく読めます。
1-5. 計測結果をもとに処理を高速化したい
処理時間を測る目的は、単に数値を知ることだけではありません。最終的には、遅い処理を見つけて改善することが目的です。
たとえば、同じ処理を何度も実行している、不要なメモリ確保が多い、コレクションの選び方が適切でない、LINQを使いすぎている、といった原因が見つかることがあります。
計測して、比較して、改善する。この流れを作ることで、C#の処理速度を効率よく改善できます。
2. C#で処理時間を計測する基本
2-1. 処理時間計測とは何を測ることか
処理時間計測とは、ある処理の開始から終了までにかかった時間を測ることです。
たとえば、次のような処理が計測対象になります。
C#var sw = Stopwatch.StartNew();
for (int i = 0; i < 1000000; i++)
{
// 計測したい処理
}
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
この場合、forループの開始から終了までの経過時間を測っています。
重要なのは、「どこからどこまでを測るのか」を明確にすることです。計測範囲があいまいだと、結果の意味もあいまいになります。
2-2. 実行時間・経過時間・CPU時間の違い
処理時間といっても、いくつかの考え方があります。
実行時間は、一般的に処理が始まってから終わるまでの時間を指します。経過時間もほぼ同じ意味で使われることが多く、Stopwatchで測るのはこの経過時間です。
一方、CPU時間は、その処理が実際にCPUを使っていた時間を指します。たとえば、ファイル読み込みやネットワーク待機のような処理では、経過時間は長くてもCPU時間は短い場合があります。
通常のアプリケーション開発で「この処理に何秒かかったか」を知りたい場合は、Stopwatchで経過時間を測れば十分です。
2-3. 計測前に確認すべき実行環境
C#の処理時間は、実行環境によって変わります。同じコードでも、次の条件によって結果が変動します。
CPUの性能
メモリ容量
ストレージの種類
OSの状態
他に動作しているプロセス
.NETのバージョン
DebugビルドかReleaseビルドか
そのため、計測結果を見るときは、どの環境で測ったのかを意識する必要があります。
特に、開発中のPCで計測した結果と、本番環境での結果は異なることがあります。性能改善を行う場合は、できるだけ本番に近い環境で計測するのが理想です。
2-4. Debug実行とRelease実行で結果が変わる理由
C#の処理時間を測るときに注意したいのが、Debug実行とRelease実行の違いです。
Debugビルドは、デバッグしやすいように最適化が抑えられています。そのため、処理時間がReleaseビルドより遅くなることがあります。
一方、ReleaseビルドではコンパイラやJITによる最適化が行われるため、実際の実行速度に近い結果を得やすくなります。
処理速度を比較したい場合や、高速化の効果を確認したい場合は、Releaseビルドで実行するのが基本です。
3. Stopwatchで処理時間を計測する方法
3-1. Stopwatchクラスとは
Stopwatchは、C#で経過時間を計測するためのクラスです。名前の通り、ストップウォッチのように開始、停止、リセットを行い、処理にかかった時間を取得できます。
StopwatchはSystem.Diagnostics名前空間に含まれています。
主に次のような用途で使います。
メソッドの実行時間を測る
ループ処理の処理時間を測る
複数の実装の速度を比較する
ファイル処理や非同期処理の時間を測る
パフォーマンス改善の前後を比較する
3-2. Stopwatchを使うために必要な名前空間
Stopwatchを使うには、ファイルの先頭に次の名前空間を追加します。
C#using System.Diagnostics;
これにより、Stopwatchクラスをそのまま使えるようになります。
C#using System;
using System.Diagnostics;
class Program
{
static void Main()
{
var sw = new Stopwatch();
sw.Start();
// 計測したい処理
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
}
}
3-3. Start・Stopで基本的な処理時間を測る
もっとも基本的な使い方は、Startで計測を開始し、Stopで計測を終了する方法です。
C#using System;
using System.Diagnostics;
using System.Threading;
class Program
{
static void Main()
{
var sw = new Stopwatch();
sw.Start();
Thread.Sleep(1000);
sw.Stop();
Console.WriteLine($"処理時間: {sw.ElapsedMilliseconds} ms");
}
}
このコードでは、Thread.Sleep(1000)によって約1秒待機し、その時間を計測しています。
ElapsedMillisecondsを使うと、経過時間をミリ秒単位で取得できます。
3-4. StartNewで簡潔に計測を始める
Stopwatch.StartNew()を使うと、インスタンスの作成と開始を同時に行えます。
C#var sw = Stopwatch.StartNew();
// 計測したい処理
Thread.Sleep(500);
sw.Stop();
Console.WriteLine($"処理時間: {sw.ElapsedMilliseconds} ms");
new Stopwatch()を作成してからStart()を呼ぶよりも簡潔に書けるため、実務ではStartNew()がよく使われます。
3-5. Restartで同じStopwatchを再利用する
Restart()を使うと、現在の計測結果をリセットして、すぐに再計測を開始できます。
C#var sw = Stopwatch.StartNew();
Thread.Sleep(300);
sw.Stop();
Console.WriteLine($"1回目: {sw.ElapsedMilliseconds} ms");
sw.Restart();
Thread.Sleep(500);
sw.Stop();
Console.WriteLine($"2回目: {sw.ElapsedMilliseconds} ms");
複数の処理を順番に測りたい場合、同じStopwatchを使い回せるので便利です。
3-6. Resetで計測結果を初期化する
Reset()は、計測結果をゼロに戻します。ただし、Restart()と違って自動的に計測は開始されません。
C#var sw = Stopwatch.StartNew();
Thread.Sleep(300);
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
sw.Reset();
Console.WriteLine(sw.ElapsedMilliseconds);
sw.Start();
Thread.Sleep(200);
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
Reset()は初期化だけを行い、再度計測したい場合はStart()を呼ぶ必要があります。
4. Stopwatchの計測結果を取得する方法
4-1. ElapsedでTimeSpanとして取得する
Elapsedプロパティを使うと、計測結果をTimeSpanとして取得できます。
C#var sw = Stopwatch.StartNew();
Thread.Sleep(1234);
sw.Stop();
TimeSpan elapsed = sw.Elapsed;
Console.WriteLine(elapsed);
出力例は次のようになります。
00:00:01.2345678
TimeSpanとして取得できるため、時間、分、秒、ミリ秒などを柔軟に扱えます。
C#Console.WriteLine($"秒: {elapsed.Seconds}");
Console.WriteLine($"ミリ秒: {elapsed.Milliseconds}");
Console.WriteLine($"合計秒: {elapsed.TotalSeconds}");
4-2. ElapsedMillisecondsでミリ秒を取得する
ElapsedMillisecondsは、経過時間をミリ秒単位の整数で取得します。
C#var sw = Stopwatch.StartNew();
Thread.Sleep(1500);
sw.Stop();
Console.WriteLine($"{sw.ElapsedMilliseconds} ms");
出力例は次のようになります。
1500 ms
整数で扱いやすいため、ログ出力や簡単な比較に向いています。
ただし、小数点以下のミリ秒は切り捨てられるため、非常に短い処理を測る場合はElapsed.TotalMillisecondsやElapsedTicksも検討しましょう。
4-3. ElapsedTicksでより細かい単位を確認する
ElapsedTicksを使うと、Stopwatchが内部的に持っているタイマーのTick数を取得できます。
C#var sw = Stopwatch.StartNew();
Thread.Sleep(100);
sw.Stop();
Console.WriteLine($"Ticks: {sw.ElapsedTicks}");
Console.WriteLine($"Frequency: {Stopwatch.Frequency}");
Stopwatch.Frequencyは、1秒あたりのTick数を表します。
秒に変換したい場合は、次のように計算できます。
C#double seconds = (double)sw.ElapsedTicks / Stopwatch.Frequency;
Console.WriteLine($"{seconds} 秒");
非常に短い処理の比較では、ミリ秒よりもTick数の方が差を確認しやすい場合があります。
4-4. TotalSeconds・TotalMillisecondsとの違い
ElapsedMillisecondsとElapsed.TotalMillisecondsは似ていますが、意味が少し違います。
C#Console.WriteLine(sw.ElapsedMilliseconds);
Console.WriteLine(sw.Elapsed.TotalMilliseconds);
ElapsedMillisecondsは整数のミリ秒です。
1234
Elapsed.TotalMillisecondsは小数を含む合計ミリ秒です。
1234.5678
同じように、Elapsed.SecondsとElapsed.TotalSecondsも違います。
Elapsed.Secondsは秒の部分だけを表します。たとえば1分10秒なら10です。
一方、Elapsed.TotalSecondsは全体を秒に換算します。1分10秒なら70です。
処理時間の比較には、基本的にTotalMillisecondsやTotalSecondsを使うとわかりやすいです。
4-5. Console.WriteLineで見やすく出力する方法
計測結果は、見やすい形式で出力すると分析しやすくなります。
C#var sw = Stopwatch.StartNew();
Thread.Sleep(1234);
sw.Stop();
Console.WriteLine($"処理時間: {sw.Elapsed.TotalMilliseconds:F2} ms");
Console.WriteLine($"処理時間: {sw.Elapsed.TotalSeconds:F3} 秒");
出力例です。
処理時間: 1234.56 ms
処理時間: 1.235 秒
F2やF3を指定すると、小数点以下の桁数をそろえられます。
C#Console.WriteLine($"処理A: {sw.Elapsed.TotalMilliseconds,10:F2} ms");
複数の結果を並べて比較する場合は、桁をそろえて出力すると見やすくなります。
5. 実用的な処理時間計測のサンプルコード
5-1. ループ処理の実行時間を測る
ループ処理の実行時間を測る基本例です。
C#using System;
using System.Diagnostics;
class Program
{
static void Main()
{
var sw = Stopwatch.StartNew();
long total = 0;
for (int i = 0; i < 10_000_000; i++)
{
total += i;
}
sw.Stop();
Console.WriteLine($"合計: {total}");
Console.WriteLine($"処理時間: {sw.Elapsed.TotalMilliseconds:F2} ms");
}
}
ループ回数が少ないと計測結果が小さすぎて差が見えにくいことがあります。その場合は、回数を増やして測ると比較しやすくなります。
5-2. メソッド単位で処理時間を測る
処理をメソッド単位で測ると、どの機能が遅いのかを把握しやすくなります。
C#using System;
using System.Diagnostics;
using System.Threading;
class Program
{
static void Main()
{
Measure("LoadData", LoadData);
Measure("Calculate", Calculate);
Measure("SaveData", SaveData);
}
static void Measure(string name, Action action)
{
var sw = Stopwatch.StartNew();
action();
sw.Stop();
Console.WriteLine($"{name}: {sw.Elapsed.TotalMilliseconds:F2} ms");
}
static void LoadData()
{
Thread.Sleep(300);
}
static void Calculate()
{
Thread.Sleep(500);
}
static void SaveData()
{
Thread.Sleep(200);
}
}
共通のMeasureメソッドを作っておくと、さまざまな処理を簡単に計測できます。
5-3. 複数処理の処理時間を比較する
異なる実装の処理時間を比較する例です。
C#using System;
using System.Diagnostics;
using System.Text;
class Program
{
static void Main()
{
const int count = 10000;
var sw1 = Stopwatch.StartNew();
string text = "";
for (int i = 0; i < count; i++)
{
text += i.ToString();
}
sw1.Stop();
var sw2 = Stopwatch.StartNew();
var sb = new StringBuilder();
for (int i = 0; i < count; i++)
{
sb.Append(i);
}
string result = sb.ToString();
sw2.Stop();
Console.WriteLine($"string連結: {sw1.Elapsed.TotalMilliseconds:F2} ms");
Console.WriteLine($"StringBuilder: {sw2.Elapsed.TotalMilliseconds:F2} ms");
}
}
文字列を大量に連結する場合、StringBuilderの方が高速になるケースが多くあります。ただし、少数回の連結なら通常の+でも十分な場合があります。
5-4. 非同期処理の処理時間を測る
Stopwatchは非同期処理でも使えます。
C#using System;
using System.Diagnostics;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
var sw = Stopwatch.StartNew();
await Task.Delay(1000);
sw.Stop();
Console.WriteLine($"非同期処理の時間: {sw.Elapsed.TotalMilliseconds:F2} ms");
}
}
awaitしている間の待機時間も含めて、開始から終了までの経過時間を測れます。
HTTPリクエストの時間を測る場合は、次のように書けます。
C#using var client = new HttpClient();
var sw = Stopwatch.StartNew();
string html = await client.GetStringAsync("https://example.com");
sw.Stop();
Console.WriteLine($"取得時間: {sw.Elapsed.TotalSeconds:F3} 秒");
ネットワーク処理は環境によって結果が大きく変わるため、複数回測って判断することが大切です。
5-5. ファイル読み書きの処理時間を測る
ファイル処理の時間を測る例です。
C#using System;
using System.Diagnostics;
using System.IO;
class Program
{
static void Main()
{
string path = "sample.txt";
var swWrite = Stopwatch.StartNew();
File.WriteAllText(path, new string('A', 1_000_000));
swWrite.Stop();
var swRead = Stopwatch.StartNew();
string text = File.ReadAllText(path);
swRead.Stop();
Console.WriteLine($"書き込み時間: {swWrite.Elapsed.TotalMilliseconds:F2} ms");
Console.WriteLine($"読み込み時間: {swRead.Elapsed.TotalMilliseconds:F2} ms");
}
}
ファイルI/Oは、ストレージ性能やOSのキャッシュの影響を受けます。1回目と2回目で結果が変わることも多いため、複数回計測して平均を見るとよいでしょう。
5-6. LINQ処理の処理時間を測る
LINQとfor文の処理時間を比較する例です。
C#using System;
using System.Diagnostics;
using System.Linq;
class Program
{
static void Main()
{
var numbers = Enumerable.Range(1, 10_000_000).ToArray();
var swLinq = Stopwatch.StartNew();
var sum1 = numbers
.Where(x => x % 2 == 0)
.Sum(x => (long)x);
swLinq.Stop();
var swFor = Stopwatch.StartNew();
long sum2 = 0;
for (int i = 0; i < numbers.Length; i++)
{
if (numbers[i] % 2 == 0)
{
sum2 += numbers[i];
}
}
swFor.Stop();
Console.WriteLine($"LINQ: {swLinq.Elapsed.TotalMilliseconds:F2} ms");
Console.WriteLine($"for文: {swFor.Elapsed.TotalMilliseconds:F2} ms");
}
}
LINQは読みやすいコードを書きやすい反面、ケースによってはfor文より遅くなることがあります。可読性とパフォーマンスのバランスを考えて使い分けることが大切です。
6. 正確に計測するための注意点
6-1. 1回だけの計測結果を信用しすぎない
処理時間は毎回まったく同じにはなりません。OSの状態、CPUの負荷、メモリ状況、GC、ディスクキャッシュなどの影響を受けるためです。
そのため、1回だけの計測結果で判断するのは危険です。
C#for (int i = 0; i < 10; i++)
{
var sw = Stopwatch.StartNew();
TargetProcess();
sw.Stop();
Console.WriteLine($"{i + 1}回目: {sw.Elapsed.TotalMilliseconds:F2} ms");
}
複数回測定し、平均値や最小値を確認すると、より信頼できる結果になります。
6-2. ウォームアップ実行を行う
C#では、初回実行時にJITコンパイルなどの影響で処理時間が長くなることがあります。そのため、正確に測りたい場合は、本番の計測前にウォームアップ実行を行うとよいです。
C#TargetProcess();
var sw = Stopwatch.StartNew();
TargetProcess();
sw.Stop();
Console.WriteLine($"処理時間: {sw.Elapsed.TotalMilliseconds:F2} ms");
最初の1回は計測に含めず、2回目以降の結果を見ることで、初回だけの影響を減らせます。
6-3. JITコンパイルの影響を考慮する
.NETでは、プログラムの実行時に中間言語がネイティブコードへコンパイルされます。これをJITコンパイルと呼びます。
初めて呼び出されるメソッドでは、JITコンパイルの時間が含まれることがあります。そのため、初回実行だけが遅く見える場合があります。
パフォーマンス比較を行う場合は、事前に対象メソッドを一度実行してから計測するのがよいでしょう。
6-4. ガベージコレクションの影響を理解する
C#では、不要になったオブジェクトのメモリをガベージコレクションが自動的に回収します。これは便利な仕組みですが、計測中にGCが発生すると処理時間に影響します。
たとえば、大量のオブジェクトを作成する処理では、GCの影響で処理時間がばらつくことがあります。
C#var list = new List<string>();
for (int i = 0; i < 100000; i++)
{
list.Add(i.ToString());
}
このような処理では、メモリ確保の量もパフォーマンスに関係します。単純な実行時間だけでなく、メモリ使用量も確認するとより正確に分析できます。
6-5. OSや他プロセスの負荷を避ける
処理時間を測るときは、できるだけ他のアプリケーションの影響を受けない状態で実行しましょう。
たとえば、次のような状況では結果が不安定になりやすいです。
大きなファイルをコピーしている
ウイルススキャンが動いている
ブラウザで大量のタブを開いている
CPU使用率が高いアプリが動いている
バックグラウンド更新が走っている
厳密な測定をしたい場合は、不要なアプリを閉じてから計測するとよいでしょう。
6-6. 短すぎる処理は繰り返して平均を取る
非常に短い処理は、1回だけ測っても正確な差がわかりにくいです。
たとえば、単純な加算処理を1回だけ測っても、ほとんど意味のある結果にはなりません。
そのような場合は、処理を何万回、何百万回と繰り返して測定します。
C#const int loopCount = 1_000_000;
var sw = Stopwatch.StartNew();
for (int i = 0; i < loopCount; i++)
{
TargetProcess();
}
sw.Stop();
double average = sw.Elapsed.TotalMilliseconds / loopCount;
Console.WriteLine($"合計時間: {sw.Elapsed.TotalMilliseconds:F2} ms");
Console.WriteLine($"1回あたり: {average:F6} ms");
1回あたりの平均時間を出せば、短い処理でも比較しやすくなります。
6-7. ベンチマーク時はReleaseビルドで実行する
処理速度を正しく比較するには、Releaseビルドで実行することが重要です。
Visual Studioを使っている場合は、ツールバーの構成をDebugからReleaseに変更して実行します。
さらに、デバッガーを接続した状態では結果に影響が出ることもあります。より実際に近い結果を見たい場合は、Releaseビルドした実行ファイルを直接起動して計測するとよいでしょう。
7. DateTimeで処理時間を測る方法とStopwatchとの違い
7-1. DateTime.Nowを使った計測方法
DateTime.Nowでも処理時間を測ることはできます。
C#DateTime start = DateTime.Now;
// 計測したい処理
Thread.Sleep(1000);
DateTime end = DateTime.Now;
TimeSpan elapsed = end - start;
Console.WriteLine($"処理時間: {elapsed.TotalMilliseconds} ms");
開始時刻と終了時刻の差分を計算することで、経過時間を求めています。
簡単な処理時間の確認であれば、この方法でも使えます。
7-2. DateTime計測が向いているケース
DateTimeによる計測は、処理の正確なパフォーマンス測定よりも、時刻を記録したいケースに向いています。
たとえば、次のような用途です。
C#DateTime start = DateTime.Now;
Console.WriteLine($"処理開始: {start:yyyy-MM-dd HH:mm:ss}");
RunBatch();
DateTime end = DateTime.Now;
Console.WriteLine($"処理終了: {end:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($"処理時間: {(end - start).TotalSeconds:F1} 秒");
バッチ処理の開始時刻や終了時刻をログに残したい場合は、DateTimeが便利です。
7-3. 高精度な計測にStopwatchが向いている理由
処理時間を正確に測りたい場合は、Stopwatchを使うべきです。
DateTime.Nowは現在時刻を取得するためのものであり、システム時刻の変更や時刻同期の影響を受ける可能性があります。
一方、Stopwatchは経過時間の計測に特化しているため、処理時間の測定に適しています。
特に、次のような場面ではStopwatchが向いています。
高速な処理を測る
複数の実装を比較する
ミリ秒未満の差を見たい
パフォーマンス改善の効果を確認する
7-4. DateTimeとStopwatchの使い分け
DateTimeとStopwatchは、目的に応じて使い分けます。
処理時間を測るならStopwatchを使います。
C#var sw = Stopwatch.StartNew();
RunProcess();
sw.Stop();
Console.WriteLine($"処理時間: {sw.Elapsed.TotalMilliseconds:F2} ms");
処理の開始日時や終了日時を記録したいならDateTimeを使います。
C#Console.WriteLine($"開始日時: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
つまり、経過時間はStopwatch、時刻の記録はDateTimeと考えるとわかりやすいです。
8. 処理時間を比較・分析するコツ
8-1. 平均値・最小値・最大値を出す
処理時間はばらつくため、1回の結果ではなく、平均値・最小値・最大値を見ると判断しやすくなります。
C#using System;
using System.Diagnostics;
using System.Linq;
class Program
{
static void Main()
{
var results = new double[10];
for (int i = 0; i < results.Length; i++)
{
var sw = Stopwatch.StartNew();
TargetProcess();
sw.Stop();
results[i] = sw.Elapsed.TotalMilliseconds;
}
Console.WriteLine($"平均: {results.Average():F2} ms");
Console.WriteLine($"最小: {results.Min():F2} ms");
Console.WriteLine($"最大: {results.Max():F2} ms");
}
static void TargetProcess()
{
Thread.Sleep(100);
}
}
平均値だけでなく、最小値と最大値を見ることで、どの程度ばらついているかもわかります。
8-2. 外れ値を除外して判断する
計測結果には、たまたま大きく遅くなった値が混ざることがあります。これを外れ値と呼びます。
たとえば、10回の計測結果が次のようになったとします。
10 ms
11 ms
10 ms
12 ms
10 ms
200 ms
11 ms
10 ms
12 ms
11 ms
この場合、200 msはGCやOS負荷などによる一時的な外れ値かもしれません。
外れ値を含めた平均だけを見ると、実態より遅く見えることがあります。状況に応じて、最小値、中央値、外れ値を除いた平均を確認するとよいでしょう。
8-3. 計測対象以外の処理を含めない
処理時間を測るときは、計測したい処理だけを範囲に含めることが重要です。
悪い例です。
C#var sw = Stopwatch.StartNew();
var data = LoadData();
var result = Calculate(data);
Console.WriteLine(result);
sw.Stop();
この場合、データ読み込み、計算、コンソール出力がすべて含まれています。計算処理だけを測りたいなら、範囲を分けます。
C#var data = LoadData();
var sw = Stopwatch.StartNew();
var result = Calculate(data);
sw.Stop();
Console.WriteLine(result);
Console.WriteLine($"計算時間: {sw.Elapsed.TotalMilliseconds:F2} ms");
何を測っているのかを明確にすることが、正しい分析の第一歩です。
8-4. ログ出力の処理時間に注意する
Console.WriteLineやファイルへのログ出力は、それ自体が時間のかかる処理です。
特に、ループの中でログ出力を行うと、計測結果に大きく影響します。
C#var sw = Stopwatch.StartNew();
for (int i = 0; i < 10000; i++)
{
Console.WriteLine(i);
}
sw.Stop();
このコードでは、ループ処理というより、ほとんどコンソール出力の時間を測っていることになります。
処理そのものの速度を測りたい場合は、ログ出力を計測範囲の外に出すか、必要最小限にしましょう。
8-5. 計測結果を表形式で整理する
複数の処理を比較するときは、表形式で整理するとわかりやすくなります。
処理名 平均時間 最小時間 最大時間
-----------------------------------------------------
for文 12.30 ms 11.80 ms 13.10 ms
LINQ 25.40 ms 24.90 ms 27.00 ms
StringBuilder 3.20 ms 3.00 ms 3.50 ms
表にすると、どの処理が遅いのか、どの処理が安定しているのかを比較しやすくなります。
計測結果をログやCSVに出力しておけば、あとからExcelなどで分析することもできます。
9. C#の処理時間を高速化する基本テクニック
9-1. 不要なループや重複処理を減らす
処理時間を改善する基本は、不要な処理を減らすことです。
たとえば、ループの中で毎回同じ計算をしている場合は、ループの外に出せる可能性があります。
C#// 改善前
for (int i = 0; i < items.Count; i++)
{
var rate = GetRate();
items[i].Price *= rate;
}
GetRate()の結果が毎回同じなら、次のようにできます。
C#// 改善後
var rate = GetRate();
for (int i = 0; i < items.Count; i++)
{
items[i].Price *= rate;
}
小さな改善でも、ループ回数が多い場合は大きな差になることがあります。
9-2. 文字列連結はStringBuilderを検討する
大量の文字列を繰り返し連結する場合は、StringBuilderを検討しましょう。
C#var text = "";
for (int i = 0; i < 10000; i++)
{
text += i + ",";
}
このようなコードでは、文字列の再生成が何度も発生します。
改善例です。
C#var sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
sb.Append(i);
sb.Append(',');
}
var text = sb.ToString();
ただし、少ない回数の連結であれば、StringBuilderにするメリットが小さい場合もあります。実際にStopwatchで比較して判断しましょう。
9-3. LINQとfor文のパフォーマンスを比較する
LINQはコードを簡潔に書ける便利な機能ですが、パフォーマンスが重要な箇所ではfor文の方が速い場合があります。
C#var result = numbers.Where(x => x > 100).Select(x => x * 2).ToList();
可読性を優先するならLINQは有効です。
一方、大量データを扱うホットパスでは、次のようにfor文で書いた方が速いケースがあります。
C#var result = new List<int>();
for (int i = 0; i < numbers.Length; i++)
{
if (numbers[i] > 100)
{
result.Add(numbers[i] * 2);
}
}
どちらがよいかは状況によります。読みやすさと速度のバランスを見ながら選びましょう。
9-4. コレクションの選び方を見直す
コレクションの選び方も処理時間に影響します。
たとえば、要素の存在確認を頻繁に行う場合、List<T>よりHashSet<T>の方が向いていることがあります。
C#var list = new List<int> { 1, 2, 3, 4, 5 };
bool exists = list.Contains(5);
件数が少なければ問題ありませんが、件数が多く、何度も検索する場合はHashSet<T>を検討できます。
C#var set = new HashSet<int> { 1, 2, 3, 4, 5 };
bool exists = set.Contains(5);
データの追加、削除、検索、順序保持など、目的に合ったコレクションを選ぶことが重要です。
9-5. メモリ確保を減らしてGC負荷を下げる
大量のオブジェクトを作成すると、ガベージコレクションの負荷が増え、処理時間が長くなることがあります。
たとえば、ループ内で毎回新しいリストを作るようなコードは注意が必要です。
C#for (int i = 0; i < 10000; i++)
{
var list = new List<int>();
list.Add(i);
}
必要に応じて、オブジェクトの再利用や、初期容量の指定を検討します。
C#var list = new List<int>(10000);
for (int i = 0; i < 10000; i++)
{
list.Add(i);
}
List<T>に追加する件数があらかじめわかっている場合は、初期容量を指定することで再確保の回数を減らせます。
9-6. 非同期処理・並列処理を適切に使う
時間のかかるI/O処理では、非同期処理が有効です。
C#string text = await File.ReadAllTextAsync("sample.txt");
非同期処理を使うと、待ち時間の間にスレッドを効率よく使えるため、アプリケーション全体の応答性を改善できます。
CPU負荷の高い処理では、並列処理を検討できます。
C#Parallel.For(0, 1000000, i =>
{
DoWork(i);
});
ただし、並列処理は常に速くなるわけではありません。処理の分割コストや同期コストが増える場合もあります。Stopwatchで実際に測り、効果があるか確認することが大切です。
10. より本格的に計測するならBenchmarkDotNetを使う
10-1. Stopwatchだけでは限界があるケース
Stopwatchは手軽で便利ですが、厳密なベンチマークには限界があります。
たとえば、次のようなケースです。
ナノ秒レベルの差を比較したい
JITやGCの影響をできるだけ除外したい
複数の実装を統計的に比較したい
実行環境の情報も含めて結果を残したい
マイクロベンチマークを正確に行いたい
このような場合は、Stopwatchで手動計測するより、ベンチマーク専用ライブラリを使った方が適しています。
10-2. BenchmarkDotNetとは
BenchmarkDotNetは、.NET向けのベンチマークライブラリです。C#のメソッドに属性を付けることで、処理時間やメモリ割り当てなどを計測できます。
簡単な例です。
C#using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Text;
public class StringBenchmark
{
[Benchmark]
public string StringConcat()
{
string text = "";
for (int i = 0; i < 1000; i++)
{
text += i;
}
return text;
}
[Benchmark]
public string StringBuilderConcat()
{
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
sb.Append(i);
}
return sb.ToString();
}
}
public class Program
{
public static void Main(string[] args)
{
BenchmarkRunner.Run<StringBenchmark>();
}
}
実行すると、平均時間や誤差、標準偏差などを含む結果が出力されます。
10-3. StopwatchとBenchmarkDotNetの違い
Stopwatchは、任意の処理を手軽に測るための標準クラスです。ログ出力や簡単な比較には十分使えます。
一方、BenchmarkDotNetは、ベンチマークを正確に行うための仕組みを提供します。
たとえば、次のような違いがあります。
Stopwatch:
・手軽に使える
・コード内にすぐ書ける
・簡単な処理時間確認に向いている
・計測方法は自分で工夫する必要がある
BenchmarkDotNet:
・本格的なベンチマークに向いている
・ウォームアップや複数回実行を自動で行う
・統計情報を出してくれる
・メモリ割り当ても確認できる
日常的な確認にはStopwatch、厳密な性能比較にはBenchmarkDotNetという使い分けがおすすめです。
10-4. マイクロベンチマークにBenchmarkDotNetが向いている理由
マイクロベンチマークとは、非常に小さな処理の性能を測ることです。
たとえば、次のような比較です。
forとforeachの差LINQと
for文の差string連結とStringBuilderの差List<T>と配列の差メソッド呼び出しのわずかな差
このような小さな差は、Stopwatchで単純に測ると誤差の影響を受けやすくなります。
BenchmarkDotNetは、ウォームアップ、繰り返し実行、統計処理などを行ってくれるため、小さな差を比較する用途に向いています。
10-5. 導入を検討すべきタイミング
BenchmarkDotNetの導入を検討すべきタイミングは、次のような場合です。
パフォーマンス改善を本格的に行いたい
複数の実装の速度差を正確に比較したい
処理時間だけでなくメモリ割り当ても確認したい
チームでベンチマーク結果を共有したい
ライブラリや共通部品の性能を検証したい
一方で、アプリケーションの一部処理が「だいたい何秒かかっているか」を見たいだけなら、まずはStopwatchで十分です。
11. C#の処理時間計測でよくある失敗
11-1. Debugビルドで計測してしまう
もっともよくある失敗は、Debugビルドのまま処理時間を測ってしまうことです。
Debugビルドでは最適化が抑えられているため、Releaseビルドより遅くなることがあります。
パフォーマンス計測では、基本的にReleaseビルドで実行しましょう。
11-2. 初回実行だけを見て判断してしまう
初回実行は、JITコンパイルやキャッシュの影響で遅くなることがあります。
そのため、初回の結果だけを見て「この処理は遅い」と判断するのは危険です。
ウォームアップを行い、複数回計測してから判断しましょう。
11-3. Console出力まで計測対象に含めてしまう
Console.WriteLineは意外と時間がかかります。計測範囲に含めると、本来測りたい処理ではなく、出力処理の時間を測ってしまうことがあります。
悪い例です。
C#var sw = Stopwatch.StartNew();
for (int i = 0; i < 10000; i++)
{
Console.WriteLine(i);
}
sw.Stop();
計測したい処理とログ出力は、できるだけ分けて考えましょう。
11-4. 短すぎる処理を単発で測ってしまう
短すぎる処理は、1回だけ測っても正確な結果が得られません。
たとえば、単純な計算やプロパティアクセスを1回だけ測っても、ほとんど意味のある差は見えません。
短い処理は、何度も繰り返して平均を取るか、BenchmarkDotNetを使うのがおすすめです。
11-5. 計測結果の単位を誤解してしまう
ElapsedMilliseconds、ElapsedTicks、Elapsed.TotalMillisecondsなどは、それぞれ意味が異なります。
特に、ElapsedTicksはTimeSpanのTicksとは意味が異なる場合があるため注意が必要です。
処理時間をミリ秒で見たいなら、基本的には次のどちらかを使うとわかりやすいです。
C#sw.ElapsedMilliseconds
sw.Elapsed.TotalMilliseconds
整数でよければElapsedMilliseconds、小数まで見たいならElapsed.TotalMillisecondsを使いましょう。
12. C#の処理時間計測に関するよくある質問
12-1. Stopwatchはどのくらい正確ですか
Stopwatchは、C#で処理時間を計測する標準的な方法として十分に高精度です。通常のアプリケーション開発で、メソッドやループの実行時間を測る用途であれば問題なく使えます。
ただし、計測結果はOSのスケジューリング、CPU負荷、GC、JITコンパイルなどの影響を受けます。
そのため、Stopwatch自体が高精度でも、1回の計測結果だけを完全に信用するのではなく、複数回測定して判断することが大切です。
12-2. ElapsedMillisecondsとElapsed.TotalMillisecondsは何が違いますか
ElapsedMillisecondsは、経過時間をミリ秒単位の整数で返します。
C#long ms = sw.ElapsedMilliseconds;
一方、Elapsed.TotalMillisecondsは、小数を含むミリ秒をdoubleで返します。
C#double ms = sw.Elapsed.TotalMilliseconds;
たとえば、実際の経過時間が123.456 msだった場合、ElapsedMillisecondsでは123、Elapsed.TotalMillisecondsでは123.456のように取得できます。
細かい差を見たい場合は、Elapsed.TotalMillisecondsを使うとよいでしょう。
12-3. 非同期処理でもStopwatchは使えますか
はい、非同期処理でもStopwatchは使えます。
C#var sw = Stopwatch.StartNew();
await SomeAsyncMethod();
sw.Stop();
Console.WriteLine($"処理時間: {sw.Elapsed.TotalMilliseconds:F2} ms");
awaitで待機している時間も含めて、開始から終了までの経過時間を測れます。
HTTP通信、ファイルI/O、データベースアクセスなどの非同期処理の処理時間を確認したい場合にも有効です。
12-4. 処理時間が毎回変わるのはなぜですか
処理時間が毎回変わるのは自然なことです。
主な理由は次の通りです。
CPUの使用状況が変わる
他のプロセスが動いている
GCが発生する
JITコンパイルの影響を受ける
ディスクやネットワークの状態が変わる
OSのスケジューリングの影響を受ける
そのため、処理時間は1回だけでなく、複数回測って平均値や中央値を見ることが大切です。
12-5. どのくらい遅ければ改善すべきですか
「何ミリ秒以上なら改善すべき」という絶対的な基準はありません。重要なのは、その処理がユーザー体験やシステム全体にどの程度影響しているかです。
たとえば、1回だけ実行される100ミリ秒の処理は問題にならないかもしれません。しかし、1回1ミリ秒でも、1万回実行される処理なら大きな負荷になります。
改善すべきかどうかは、次の観点で判断するとよいでしょう。
ユーザーの待ち時間に影響しているか
頻繁に実行される処理か
サーバー負荷やコストに影響しているか
他の処理のボトルネックになっているか
改善コストに見合う効果があるか
まずはStopwatchで計測し、本当に遅い箇所を見つけてから改善することが重要です。
まとめ
C#で処理時間を計測するなら、基本的にはStopwatchを使うのがおすすめです。Stopwatchは経過時間の計測に適しており、ミリ秒単位やより細かい単位で処理時間を確認できます。
基本的な使い方は、Stopwatch.StartNew()で計測を開始し、処理が終わったらStop()を呼び、ElapsedMillisecondsやElapsed.TotalMillisecondsで結果を取得するだけです。
C#var sw = Stopwatch.StartNew();
TargetProcess();
sw.Stop();
Console.WriteLine($"処理時間: {sw.Elapsed.TotalMilliseconds:F2} ms");
ただし、正確に計測するには、1回だけの結果を信用しすぎないことが大切です。ウォームアップを行い、JITコンパイルやGCの影響を考慮し、Releaseビルドで複数回測定しましょう。
DateTime.Nowでも処理時間を測ることはできますが、経過時間の計測にはStopwatchの方が適しています。DateTimeは開始時刻や終了時刻のログ記録に使い、処理時間の測定にはStopwatchを使うとよいでしょう。
また、処理時間を測るだけでなく、結果をもとに改善することが重要です。不要なループを減らす、文字列連結を見直す、LINQとfor文を比較する、適切なコレクションを選ぶ、メモリ確保を減らすなど、基本的な高速化テクニックを組み合わせることで、C#アプリケーションのパフォーマンスを改善できます。
より本格的に性能を比較したい場合は、BenchmarkDotNetの利用も検討しましょう。日常的な処理時間の確認にはStopwatch、厳密なマイクロベンチマークにはBenchmarkDotNetという使い分けが効果的です。

