C#で処理時間を正確に計測する方法|Stopwatchの使い方とよくある落とし穴
はじめに
C#で処理時間を計測したい場合、最も基本的で使いやすい方法はStopwatchクラスを使うことです。
処理の開始時刻と終了時刻を取得して差分を計算する方法もありますが、正確な計測をしたいならDateTime.NowではなくSystem.Diagnostics.Stopwatchを使うのが一般的です。
特に、次のような場面では処理時間の計測が重要になります。
処理が遅い原因を調べたい
複数の実装方法の速度を比較したい
非同期処理にかかる時間を確認したい
本番環境で特定処理の実行時間をログに残したい
パフォーマンス改善の効果を確認したい
この記事では、C#で処理時間を正確に計測する方法として、Stopwatchの基本的な使い方から、よくある落とし穴、実務で使いやすい計測コードまで解説します。
1. C#で処理時間を計測する基本
1-1. C#で処理時間を計測したい場面
C#で処理時間を計測する場面は多くあります。
たとえば、データベースアクセス、ファイル読み込み、API通信、ループ処理、LINQの処理、画像変換、CSV出力など、処理が遅くなりやすい箇所を調べるときに計測が役立ちます。
処理時間を計測しないまま「この処理が遅そう」と判断すると、実際には別の処理がボトルネックだったということもあります。
そのため、パフォーマンス改善では、まず実際の処理時間を測ることが重要です。
1-2. DateTimeではなくStopwatchを使うべき理由
C#で処理時間を計測するときに、次のようなコードを書きたくなるかもしれません。
C#var start = DateTime.Now;
// 計測したい処理
Thread.Sleep(1000);
var end = DateTime.Now;
var elapsed = end - start;
Console.WriteLine(elapsed.TotalMilliseconds);
この方法でも大まかな時間は測れますが、正確な処理時間の計測には向いていません。
DateTime.Nowは現在時刻を取得するためのものです。システム時刻の変更、タイムゾーン、時計の補正などの影響を受ける可能性があります。
一方、Stopwatchは処理時間の計測を目的としたクラスです。高分解能パフォーマンスカウンターを利用できる環境では、より高精度な計測ができます。
そのため、C#で処理時間を計測する場合は、基本的にStopwatchを使うべきです。
1-3. Stopwatchで計測できる時間の単位
Stopwatchでは、計測結果をさまざまな単位で取得できます。
代表的な取得方法は次のとおりです。
C#var stopwatch = Stopwatch.StartNew();
// 計測したい処理
Thread.Sleep(1234);
stopwatch.Stop();
Console.WriteLine(stopwatch.Elapsed); // TimeSpan
Console.WriteLine(stopwatch.ElapsedMilliseconds); // ミリ秒
Console.WriteLine(stopwatch.ElapsedTicks); // タイマー刻み
ElapsedはTimeSpanとして取得できます。
ElapsedMillisecondsはミリ秒単位の整数値です。
ElapsedTicksはStopwatch内部のタイマー刻みで、より細かい値を扱いたい場合に使います。
2. Stopwatchの基本的な使い方
2-1. using System.Diagnosticsを追加する
Stopwatchを使うには、次の名前空間を追加します。
C#using System.Diagnostics;
StopwatchはSystem.Diagnostics名前空間に含まれています。
Visual Studioや.NETのプロジェクトでは、ファイルの先頭にusing System.Diagnostics;を追加すれば利用できます。
2-2. Start・Stopで処理時間を計測する
最も基本的な使い方は、Startで計測を開始し、Stopで計測を終了する方法です。
C#using System;
using System.Diagnostics;
using System.Threading;
class Program
{
static void Main()
{
var stopwatch = new Stopwatch();
stopwatch.Start();
// 計測したい処理
Thread.Sleep(1000);
stopwatch.Stop();
Console.WriteLine($"処理時間: {stopwatch.ElapsedMilliseconds} ms");
}
}
このコードでは、Thread.Sleep(1000)の処理時間を計測しています。
実行すると、おおよそ1000ミリ秒前後の値が表示されます。
2-3. Stopwatch.StartNewで簡潔に書く
StopwatchにはStartNewという便利なメソッドがあります。
StartNewを使うと、インスタンスの作成と計測開始を同時に行えます。
C#var stopwatch = Stopwatch.StartNew();
// 計測したい処理
Thread.Sleep(1000);
stopwatch.Stop();
Console.WriteLine($"処理時間: {stopwatch.ElapsedMilliseconds} ms");
次の2行を1行にまとめたようなイメージです。
C#var stopwatch = new Stopwatch();
stopwatch.Start();
実務では、単純な処理時間の計測であればStopwatch.StartNew()を使うことが多いです。
2-4. RestartとResetの違い
StopwatchにはRestartとResetがあります。
似ていますが、動作が異なります。
Resetは計測時間を0に戻して停止状態にします。
C#stopwatch.Reset();
Restartは計測時間を0に戻して、すぐに再スタートします。
C#stopwatch.Restart();
つまり、次のような違いがあります。
C#var stopwatch = Stopwatch.StartNew();
Thread.Sleep(500);
stopwatch.Stop();
stopwatch.Reset();
// この時点では停止中
stopwatch.Start();
Thread.Sleep(500);
stopwatch.Stop();
一方、Restartなら次のように書けます。
C#var stopwatch = Stopwatch.StartNew();
Thread.Sleep(500);
stopwatch.Restart();
// この時点で0に戻り、再び計測開始
Thread.Sleep(500);
stopwatch.Stop();
同じStopwatchインスタンスを使って複数回計測する場合は、ResetとRestartの違いを理解しておくことが大切です。
3. 計測結果の取得方法
3-1. ElapsedでTimeSpanとして取得する
Elapsedプロパティを使うと、計測結果をTimeSpanとして取得できます。
C#var stopwatch = Stopwatch.StartNew();
Thread.Sleep(1234);
stopwatch.Stop();
TimeSpan elapsed = stopwatch.Elapsed;
Console.WriteLine(elapsed);
Console.WriteLine($"秒: {elapsed.TotalSeconds}");
Console.WriteLine($"ミリ秒: {elapsed.TotalMilliseconds}");
TimeSpanとして扱えるため、秒、分、時間などに変換しやすいのが特徴です。
人間が読みやすい形式でログ出力したい場合にも便利です。
3-2. ElapsedMillisecondsでミリ秒を取得する
ElapsedMillisecondsを使うと、経過時間をミリ秒単位の整数で取得できます。
C#var stopwatch = Stopwatch.StartNew();
Thread.Sleep(1000);
stopwatch.Stop();
long milliseconds = stopwatch.ElapsedMilliseconds;
Console.WriteLine($"{milliseconds} ms");
処理時間をログに残す場合や、しきい値を超えたかどうかを判定する場合に使いやすいです。
C#if (stopwatch.ElapsedMilliseconds > 3000)
{
Console.WriteLine("処理に3秒以上かかりました。");
}
ただし、ElapsedMillisecondsは整数値なので、ミリ秒未満の情報は切り捨てられます。
短い処理を正確に比較したい場合は、Elapsed.TotalMillisecondsやElapsedTicksも検討しましょう。
3-3. ElapsedTicksでより細かく取得する
ElapsedTicksを使うと、Stopwatchのタイマー刻み単位で経過時間を取得できます。
C#var stopwatch = Stopwatch.StartNew();
Thread.Sleep(100);
stopwatch.Stop();
Console.WriteLine($"Ticks: {stopwatch.ElapsedTicks}");
Console.WriteLine($"Frequency: {Stopwatch.Frequency}");
ElapsedTicksの1 tickは、TimeSpanのtickとは異なります。
Stopwatch.ElapsedTicksは、Stopwatchが使っている高分解能タイマーの刻みです。
1秒あたりのtick数はStopwatch.Frequencyで確認できます。
C#double seconds = (double)stopwatch.ElapsedTicks / Stopwatch.Frequency;
Console.WriteLine($"秒: {seconds}");
非常に短い処理を比較したい場合に使えますが、通常の処理時間計測ではElapsedまたはElapsedMillisecondsで十分なことが多いです。
3-4. TotalMillisecondsとMillisecondsの違い
TimeSpanにはTotalMillisecondsとMillisecondsがあります。
この2つは名前が似ていますが、意味が違います。
C#var time = TimeSpan.FromSeconds(1.5);
Console.WriteLine(time.TotalMilliseconds); // 1500
Console.WriteLine(time.Milliseconds); // 500
TotalMillisecondsは、経過時間全体をミリ秒に換算した値です。
一方、Millisecondsは、秒に満たないミリ秒部分だけを返します。
たとえば1.5秒の場合、TotalMillisecondsは1500ですが、Millisecondsは500です。
処理時間をミリ秒で表示したい場合は、基本的にTotalMillisecondsまたはElapsedMillisecondsを使いましょう。
C#Console.WriteLine(stopwatch.Elapsed.TotalMilliseconds);
Console.WriteLine(stopwatch.ElapsedMilliseconds);
4. C#で処理時間を正確に計測するサンプルコード
4-1. コンソールアプリで処理時間を計測する例
まずは、コンソールアプリで処理時間を計測する基本例です。
C#using System;
using System.Diagnostics;
using System.Threading;
class Program
{
static void Main()
{
var stopwatch = Stopwatch.StartNew();
// 計測したい処理
Thread.Sleep(1500);
stopwatch.Stop();
Console.WriteLine($"処理時間: {stopwatch.ElapsedMilliseconds} ms");
Console.WriteLine($"処理時間: {stopwatch.Elapsed.TotalSeconds} 秒");
}
}
Stopwatch.StartNew()で計測を開始し、処理が終わったらStop()を呼びます。
その後、ElapsedMillisecondsやElapsed.TotalSecondsで結果を取得できます。
4-2. メソッド単位で処理時間を計測する例
実務では、特定のメソッド単位で処理時間を測りたいことが多いです。
C#using System;
using System.Diagnostics;
using System.Threading;
class Program
{
static void Main()
{
var stopwatch = Stopwatch.StartNew();
ExecuteHeavyProcess();
stopwatch.Stop();
Console.WriteLine($"ExecuteHeavyProcessの処理時間: {stopwatch.ElapsedMilliseconds} ms");
}
static void ExecuteHeavyProcess()
{
// 重い処理の例
Thread.Sleep(2000);
}
}
このように書くことで、対象メソッドだけの処理時間を確認できます。
複数のメソッドで同じように計測したい場合は、後述するヘルパークラスを用意すると便利です。
4-3. 複数処理の処理時間を比較する例
複数の実装方法を比較したい場合は、それぞれの処理を同じ条件で計測します。
C#using System;
using System.Diagnostics;
using System.Linq;
class Program
{
static void Main()
{
const int count = 1_000_000;
var stopwatch = Stopwatch.StartNew();
int sum1 = 0;
for (int i = 0; i < count; i++)
{
sum1 += i;
}
stopwatch.Stop();
Console.WriteLine($"for文: {stopwatch.Elapsed.TotalMilliseconds} ms");
stopwatch.Restart();
int sum2 = Enumerable.Range(0, count).Sum();
stopwatch.Stop();
Console.WriteLine($"LINQ: {stopwatch.Elapsed.TotalMilliseconds} ms");
}
}
この例では、for文とLINQの処理時間を比較しています。
ただし、1回だけの計測結果で性能を判断するのは危険です。
JITコンパイル、GC、CPUの状態、他プロセスの影響などで結果が変わるため、実際には複数回実行して平均や中央値を見るのが望ましいです。
4-4. async/awaitの非同期処理を計測する例
Stopwatchは非同期処理の計測にも使えます。
C#using System;
using System.Diagnostics;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
var stopwatch = Stopwatch.StartNew();
await ExecuteAsync();
stopwatch.Stop();
Console.WriteLine($"非同期処理の経過時間: {stopwatch.ElapsedMilliseconds} ms");
}
static async Task ExecuteAsync()
{
using var client = new HttpClient();
var response = await client.GetAsync("https://example.com");
var content = await response.Content.ReadAsStringAsync();
}
}
awaitしている間の待ち時間も、経過時間として計測されます。
つまり、API通信やファイルI/Oなどの待機時間を含めた「開始から終了までの実時間」を測ることができます。
5. Stopwatchで正確に計測するためのポイント
5-1. 初回実行はJITコンパイルの影響を受ける
C#のコードは、実行時にJITコンパイルされることがあります。
そのため、初回実行時はメソッドのコンパイル時間が含まれ、2回目以降より遅くなることがあります。
たとえば次のように1回だけ計測すると、初回特有のコストが混ざる可能性があります。
C#var stopwatch = Stopwatch.StartNew();
TargetMethod();
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);
より正確に測りたい場合は、本計測の前に一度対象処理を実行しておくとよいです。
C#// ウォームアップ
TargetMethod();
// 本計測
var stopwatch = Stopwatch.StartNew();
TargetMethod();
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);
5-2. 短すぎる処理は繰り返し実行して平均を取る
非常に短い処理は、1回だけ計測しても誤差の影響が大きくなります。
たとえば、数ナノ秒から数マイクロ秒程度の処理を1回だけ測っても、正確な比較は難しいです。
その場合は、処理を何度も繰り返して合計時間を測り、1回あたりの平均を求めます。
C#const int iterations = 1_000_000;
var stopwatch = Stopwatch.StartNew();
for (int i = 0; i < iterations; i++)
{
TargetMethod();
}
stopwatch.Stop();
double averageMs = stopwatch.Elapsed.TotalMilliseconds / iterations;
Console.WriteLine($"合計時間: {stopwatch.Elapsed.TotalMilliseconds} ms");
Console.WriteLine($"1回あたり: {averageMs} ms");
ただし、ループ自体のコストも含まれるため、厳密なマイクロベンチマークでは専用ツールの利用を検討しましょう。
5-3. デバッグ実行ではなくReleaseビルドで計測する
Visual Studioでデバッグ実行している場合、最適化が無効になっていたり、デバッガの影響を受けたりします。
そのため、Debugビルドでの計測結果をそのまま性能判断に使うのは避けましょう。
処理時間を比較する場合は、次の条件で計測するのが基本です。
Releaseビルドで実行する
デバッガをアタッチしない
できるだけ同じ環境で複数回実行する
不要なログ出力やコンソール出力を計測対象から外す
特にパフォーマンス改善の前後比較をする場合は、計測条件を揃えることが重要です。
5-4. GCや他プロセスの影響を考慮する
C#では、ガベージコレクション、つまりGCが発生すると処理時間に影響することがあります。
また、OS上で他のプロセスがCPUやディスクを使っている場合も、計測結果が変動します。
そのため、Stopwatchで同じ処理を測っても、毎回まったく同じ値になるとは限りません。
実務では、1回の結果だけではなく、複数回の計測結果を見て判断することが大切です。
必要に応じて、平均値、中央値、最小値、最大値などを確認するとよいでしょう。
5-5. ウォームアップを行ってから本計測する
正確な計測をしたい場合は、ウォームアップを行ってから本計測するのがおすすめです。
C#// ウォームアップ
for (int i = 0; i < 10; i++)
{
TargetMethod();
}
// 本計測
var stopwatch = Stopwatch.StartNew();
for (int i = 0; i < 1000; i++)
{
TargetMethod();
}
stopwatch.Stop();
Console.WriteLine($"処理時間: {stopwatch.Elapsed.TotalMilliseconds} ms");
ウォームアップにより、JITコンパイルや初期化処理の影響をある程度減らせます。
ただし、ウォームアップだけで完全に誤差をなくせるわけではありません。
より厳密な計測が必要な場合は、BenchmarkDotNetなどのベンチマークライブラリを使うのが適しています。
6. C#の処理時間計測でよくある落とし穴
6-1. DateTime.Nowで計測してしまう
C#で処理時間を計測するときの代表的な落とし穴が、DateTime.Nowを使ってしまうことです。
C#var start = DateTime.Now;
// 処理
var end = DateTime.Now;
var elapsed = end - start;
この方法は、現在時刻の差分を見ているだけです。
システム時刻の変更や補正の影響を受ける可能性があるため、処理時間の計測にはStopwatchを使いましょう。
C#var stopwatch = Stopwatch.StartNew();
// 処理
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);
6-2. Startを複数回呼んで累積時間に気づかない
Stopwatchは、Stopした後に再びStartすると、前回の経過時間に加算されます。
C#var stopwatch = new Stopwatch();
stopwatch.Start();
Thread.Sleep(500);
stopwatch.Stop();
stopwatch.Start();
Thread.Sleep(500);
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);
この場合、結果は約1000ミリ秒になります。
2回目だけを測りたい場合は、RestartまたはResetを使いましょう。
C#stopwatch.Restart();
Thread.Sleep(500);
stopwatch.Stop();
6-3. Resetし忘れて前回の計測結果が混ざる
同じStopwatchインスタンスを使い回す場合、Resetし忘れると前回の計測結果が混ざります。
C#var stopwatch = new Stopwatch();
stopwatch.Start();
ProcessA();
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);
stopwatch.Start();
ProcessB();
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);
このコードでは、2回目の結果にProcessAの時間も含まれます。
別々に計測したい場合は、次のようにします。
C#stopwatch.Restart();
ProcessB();
stopwatch.Stop();
または、処理ごとに新しいStopwatchを作成してもよいです。
C#var stopwatch = Stopwatch.StartNew();
ProcessB();
stopwatch.Stop();
6-4. ElapsedMillisecondsの丸めに注意する
ElapsedMillisecondsは、経過時間をミリ秒単位の整数で返します。
そのため、ミリ秒未満の情報は失われます。
C#Console.WriteLine(stopwatch.ElapsedMilliseconds);
短い処理を測る場合、ElapsedMillisecondsが0になることもあります。
より細かく見たい場合は、Elapsed.TotalMillisecondsを使いましょう。
C#Console.WriteLine(stopwatch.Elapsed.TotalMilliseconds);
TotalMillisecondsはdoubleで値を返すため、小数点以下の情報も確認できます。
6-5. Console.WriteLineなど計測外の処理が混ざる
計測対象の中にConsole.WriteLineなどの出力処理を入れると、計測結果に大きな影響を与えることがあります。
悪い例は次のとおりです。
C#var stopwatch = Stopwatch.StartNew();
for (int i = 0; i < 1000; i++)
{
Console.WriteLine(i);
}
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);
この場合、コンソール出力の時間が大きく含まれます。
純粋に計算処理だけを測りたいなら、出力処理は計測対象から外しましょう。
C#var stopwatch = Stopwatch.StartNew();
for (int i = 0; i < 1000; i++)
{
// 計測したい処理
}
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);
6-6. Debugビルドの結果を性能判断に使ってしまう
Debugビルドでは、最適化が十分に行われていないことがあります。
そのため、Debugビルドでの計測結果は、Releaseビルドと大きく異なる場合があります。
性能比較や改善効果の確認をする場合は、Releaseビルドで実行しましょう。
また、Visual Studioのデバッガを付けた状態ではなく、可能であればコマンドラインから実行するのがおすすめです。
Bashdotnet run -c Release
このようにRelease構成で実行すると、より実運用に近い条件で処理時間を確認できます。
7. Stopwatchの精度と仕組み
7-1. Stopwatchは高分解能パフォーマンスカウンターを利用する
Stopwatchは、処理時間の計測に適したタイマーを使用します。
利用可能な環境では、高分解能パフォーマンスカウンターを使って経過時間を測定します。
そのため、DateTime.Nowで現在時刻の差分を取るよりも、処理時間の計測に向いています。
C#で「正確に処理時間を計測したい」と考える場合、まず候補にすべきなのがStopwatchです。
7-2. IsHighResolutionで高精度タイマーか確認する
Stopwatch.IsHighResolutionを使うと、高分解能パフォーマンスカウンターを使用しているか確認できます。
C#Console.WriteLine(Stopwatch.IsHighResolution);
trueであれば、高分解能タイマーが使われています。
次のように表示すると、環境の情報を確認できます。
C#Console.WriteLine($"High Resolution: {Stopwatch.IsHighResolution}");
Console.WriteLine($"Frequency: {Stopwatch.Frequency}");
実務で毎回確認する必要はありませんが、Stopwatchの仕組みを理解するうえでは知っておくとよいプロパティです。
7-3. Frequencyで1秒あたりのカウント数を確認する
Stopwatch.Frequencyは、1秒あたりのタイマー刻み数を表します。
C#Console.WriteLine(Stopwatch.Frequency);
たとえば、Frequencyが10,000,000であれば、1秒あたり10,000,000カウントという意味です。
ElapsedTicksを秒に変換する場合は、次のように計算できます。
C#double seconds = (double)stopwatch.ElapsedTicks / Stopwatch.Frequency;
通常はElapsedやElapsedMillisecondsを使えば十分ですが、より細かい単位で確認したい場合にFrequencyとElapsedTicksを組み合わせます。
7-4. Stopwatchでも完全に同じ結果にならない理由
Stopwatchは高精度な計測に向いていますが、同じ処理を何度実行しても完全に同じ結果になるわけではありません。
理由としては、次のような要因があります。
JITコンパイルの影響
GCの発生
CPUの負荷変動
OSのスケジューリング
他プロセスの影響
キャッシュの状態
I/Oやネットワークの状態
そのため、Stopwatchの結果は「正確な実行時間を知るための有力な情報」ではありますが、1回だけの結果を絶対視しないことが大切です。
特に短い処理を比較する場合は、複数回測定して傾向を見るようにしましょう。
8. より正確なベンチマークが必要な場合
8-1. Stopwatchとベンチマークの違い
Stopwatchは、処理時間を手軽に測るためのクラスです。
一方、ベンチマークは、処理性能をできるだけ公平な条件で比較するための計測です。
Stopwatchでも処理時間は測れますが、次のような要素は自分で考慮する必要があります。
ウォームアップ
繰り返し回数
統計的な集計
外れ値の扱い
JITコンパイルの影響
GCの影響
DebugビルドとReleaseビルドの違い
単純な処理時間の確認であればStopwatchで十分です。
しかし、メソッドの細かな性能差を比較したい場合は、専用のベンチマークツールを使ったほうが安全です。
8-2. マイクロベンチマークではBenchmarkDotNetを検討する
C#でマイクロベンチマークを行う場合は、BenchmarkDotNetがよく使われます。
BenchmarkDotNetを使うと、ウォームアップ、繰り返し実行、統計情報の出力などを自動で行えます。
たとえば、次のような細かい比較をしたい場合に向いています。
for文とLINQの性能比較文字列結合の方法による違い
配列とListのアクセス速度比較
メソッド呼び出しのオーバーヘッド
アルゴリズムの微小な差
Stopwatchで1回だけ測った結果よりも、信頼しやすい結果を得られます。
8-3. Stopwatchが向いているケース
Stopwatchが向いているのは、手軽に経過時間を確認したいケースです。
たとえば、次のような用途に適しています。
API呼び出し全体に何秒かかったか確認する
ファイル読み込みにかかる時間をログに出す
バッチ処理の実行時間を測る
画面表示までの時間を大まかに確認する
処理改善前後の大まかな差を確認する
開発中に一時的に処理時間を確認する
実務では、Stopwatchをログ出力と組み合わせて使うと便利です。
C#var stopwatch = Stopwatch.StartNew();
ExecuteBatch();
stopwatch.Stop();
logger.LogInformation("バッチ処理時間: {ElapsedMilliseconds} ms", stopwatch.ElapsedMilliseconds);
8-4. BenchmarkDotNetが向いているケース
BenchmarkDotNetが向いているのは、より厳密に性能比較をしたいケースです。
たとえば、次のような場合です。
数マイクロ秒以下の処理を比較したい
複数の実装方法の性能差を正確に見たい
平均値、中央値、標準偏差などを確認したい
JITやGCの影響を考慮したい
ライブラリや共通部品の性能を検証したい
記事やドキュメントに載せる信頼性の高いベンチマーク結果が必要
Stopwatchは手軽な計測に向いていますが、マイクロベンチマークでは誤差の影響を受けやすいです。
細かな性能差を根拠に設計判断をする場合は、BenchmarkDotNetの利用を検討しましょう。
9. 実務で使いやすい処理時間計測の書き方
9-1. usingステートメントで計測するヘルパークラス
実務で何度も処理時間を計測する場合は、IDisposableを使ったヘルパークラスを作ると便利です。
C#using System;
using System.Diagnostics;
public sealed class ProcessingTimer : IDisposable
{
private readonly string _name;
private readonly Stopwatch _stopwatch;
public ProcessingTimer(string name)
{
_name = name;
_stopwatch = Stopwatch.StartNew();
}
public void Dispose()
{
_stopwatch.Stop();
Console.WriteLine($"{_name}: {_stopwatch.ElapsedMilliseconds} ms");
}
}
使う側は次のように書けます。
C#using (new ProcessingTimer("重い処理"))
{
ExecuteHeavyProcess();
}
usingブロックを抜けたタイミングで自動的にDisposeが呼ばれ、処理時間が出力されます。
計測開始と終了の書き忘れを防げるため、開発中の一時的な計測にも便利です。
9-2. ログ出力と組み合わせる方法
実務では、Console.WriteLineではなくロガーに出力することが多いです。
ASP.NET CoreなどでILoggerを使っている場合は、次のようなヘルパーが使えます。
C#using System;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
public sealed class LogTimer : IDisposable
{
private readonly ILogger _logger;
private readonly string _name;
private readonly Stopwatch _stopwatch;
public LogTimer(ILogger logger, string name)
{
_logger = logger;
_name = name;
_stopwatch = Stopwatch.StartNew();
}
public void Dispose()
{
_stopwatch.Stop();
_logger.LogInformation("{ProcessName} completed in {ElapsedMilliseconds} ms",
_name,
_stopwatch.ElapsedMilliseconds);
}
}
使い方は次のとおりです。
C#using (new LogTimer(_logger, "注文データ取得"))
{
await GetOrderDataAsync();
}
ログに処理名と処理時間を残しておけば、あとから遅い処理を調査しやすくなります。
9-3. 処理名つきで計測結果を出す方法
複数の処理を計測する場合は、処理名を付けて出力すると見やすくなります。
C#static void Measure(string name, Action action)
{
var stopwatch = Stopwatch.StartNew();
action();
stopwatch.Stop();
Console.WriteLine($"{name}: {stopwatch.Elapsed.TotalMilliseconds} ms");
}
次のように使えます。
C#Measure("ProcessA", () =>
{
ProcessA();
});
Measure("ProcessB", () =>
{
ProcessB();
});
非同期処理を計測したい場合は、Func<Task>を受け取るメソッドを用意します。
C#static async Task MeasureAsync(string name, Func<Task> action)
{
var stopwatch = Stopwatch.StartNew();
await action();
stopwatch.Stop();
Console.WriteLine($"{name}: {stopwatch.Elapsed.TotalMilliseconds} ms");
}
使い方は次のとおりです。
C#await MeasureAsync("API呼び出し", async () =>
{
await CallApiAsync();
});
これにより、処理ごとの計測コードを簡潔に書けます。
9-4. 開発中だけ計測コードを有効にする方法
開発中だけ処理時間を計測したい場合は、条件付きコンパイルを使う方法があります。
C##if DEBUG
var stopwatch = Stopwatch.StartNew();
#endif
ExecuteProcess();
#if DEBUG
stopwatch.Stop();
Console.WriteLine($"処理時間: {stopwatch.ElapsedMilliseconds} ms");
#endif
また、メソッドに分ける場合はConditional属性を使う方法もあります。
C#using System.Diagnostics;
public static class DebugTimer
{
[Conditional("DEBUG")]
public static void Log(string message)
{
Debug.WriteLine(message);
}
}
ただし、本番環境でもパフォーマンス監視のために処理時間をログに残したい場合があります。
その場合は、ログレベルや設定ファイルで出力の有無を切り替える設計にするとよいでしょう。
10. C#の処理時間計測に関するよくある質問
10-1. Stopwatchはミリ秒以下も計測できる?
はい、Stopwatchはミリ秒以下の計測も可能です。
ただし、実際にどの程度細かく測れるかは実行環境に依存します。
ミリ秒未満の値を確認したい場合は、Elapsed.TotalMillisecondsやElapsedTicksを使います。
C#Console.WriteLine(stopwatch.Elapsed.TotalMilliseconds);
Console.WriteLine(stopwatch.ElapsedTicks);
ElapsedMillisecondsは整数のミリ秒なので、ミリ秒未満は表現できません。
10-2. Stopwatchはスレッドセーフ?
同じStopwatchインスタンスを複数スレッドから同時に操作する使い方は避けたほうが安全です。
たとえば、あるスレッドでStartし、別のスレッドでStopやResetを呼ぶと、意図しない結果になる可能性があります。
基本的には、計測したい処理ごとにStopwatchインスタンスを作成して使いましょう。
C#var stopwatch = Stopwatch.StartNew();
// この処理だけを計測
stopwatch.Stop();
複数スレッドの処理を測りたい場合は、「全体の経過時間を測る」のか、「各スレッド内の処理時間を測る」のかを分けて考えることが重要です。
10-3. 非同期処理の待ち時間も計測される?
はい、Stopwatchは開始から終了までの経過時間を測るため、await中の待ち時間も含まれます。
C#var stopwatch = Stopwatch.StartNew();
await Task.Delay(1000);
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);
この場合、約1000ミリ秒が計測されます。
つまり、Stopwatchで測れるのはCPUが実際に動いていた時間ではなく、処理開始から処理終了までの実時間です。
API通信、ファイルI/O、データベースアクセスなどの待ち時間も含めた時間を測る場合に適しています。
10-4. 処理時間とCPU使用時間は同じ?
処理時間とCPU使用時間は同じではありません。
Stopwatchで測るのは、基本的に経過時間です。
たとえば、次のような処理では、待機時間も含めて計測されます。
C#var stopwatch = Stopwatch.StartNew();
await Task.Delay(1000);
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);
この処理は約1秒かかりますが、その間ずっとCPUを使い続けているわけではありません。
CPU使用時間を詳しく調べたい場合は、プロファイラーや診断ツールを使う必要があります。
Stopwatchは「ユーザーが待った時間」や「処理全体にかかった時間」を測るのに向いています。
10-5. 本番環境でStopwatchを使っても問題ない?
本番環境でStopwatchを使うこと自体は問題ありません。
むしろ、処理時間をログに残すことで、遅延の原因調査やパフォーマンス監視に役立ちます。
ただし、次の点には注意しましょう。
大量のログを出しすぎない
計測コードが処理を複雑にしすぎないようにする
機密情報をログに含めない
高頻度処理ではログ出力のコストにも注意する
必要に応じてログレベルで出力を制御する
たとえば、一定時間以上かかった場合だけログを出す方法があります。
C#var stopwatch = Stopwatch.StartNew();
await ExecuteProcessAsync();
stopwatch.Stop();
if (stopwatch.ElapsedMilliseconds > 1000)
{
logger.LogWarning("処理に時間がかかっています: {ElapsedMilliseconds} ms",
stopwatch.ElapsedMilliseconds);
}
このようにすれば、遅い処理だけを記録できます。
まとめ
C#で処理時間を計測するなら、基本的にはStopwatchを使うのが適切です。
DateTime.Nowでも時刻の差分から大まかな時間は測れますが、処理時間の計測には向いていません。Stopwatchは経過時間の計測を目的としたクラスであり、高分解能パフォーマンスカウンターを利用できる環境では高精度な計測が可能です。
基本的な使い方は、Stopwatch.StartNew()で計測を開始し、処理後にStop()を呼んで、ElapsedやElapsedMillisecondsで結果を取得するだけです。
C#var stopwatch = Stopwatch.StartNew();
// 計測したい処理
stopwatch.Stop();
Console.WriteLine($"処理時間: {stopwatch.ElapsedMilliseconds} ms");
ただし、正確に処理時間を計測するには、いくつかの注意点があります。
初回実行ではJITコンパイルの影響を受けることがあります。短すぎる処理は1回だけでなく繰り返し実行して平均を見る必要があります。DebugビルドではなくReleaseビルドで計測することも重要です。
また、ElapsedMillisecondsはミリ秒単位の整数なので、短い処理ではElapsed.TotalMillisecondsやElapsedTicksも活用しましょう。
手軽に処理時間を確認したい場合はStopwatchで十分です。一方、マイクロベンチマークのように厳密な性能比較が必要な場合は、BenchmarkDotNetなどの専用ツールを検討するとよいでしょう。
C#の計測では、ただ時間を測るだけでなく、計測条件を揃え、計測対象を明確にし、結果を正しく読み取ることが大切です。

