C#におけるミリ秒の扱いは、アプリケーションのパフォーマンス測定やログ出力、正確なタイミング制御において非常に重要です。

単純に現在の時刻を表示するだけでなく、ミリ秒単位での経過時間を計測したり、テストコードでの再現性を確保したりするために、C#には複数のアプローチが用意されています。

本記事では、古典的なDateTimeから、高精度な計測を可能にするStopwatch、そして現代的な開発に欠かせないTimeProviderまで、状況に応じた最適なミリ秒の取得・計測手法を解説します。

現在時刻からミリ秒を取得する方法

C#で「現在の時刻」に含まれるミリ秒を取得する場合、最も一般的なのはDateTime構造体やDateTimeOffset構造体を使用する方法です。

これらはシステム時計に基づいた時刻を保持しており、特定の瞬間のミリ秒(0~999)を取得するのに適しています。

DateTimeを使用したミリ秒の取得

DateTime.Nowを使用すると、ローカル時刻のミリ秒部分を簡単に取得できます。

以下のコードは、現在のミリ秒を数値および文字列として取得する例です。

C#
// 現在の時刻を取得
DateTime now = DateTime.Now;

// Millisecondプロパティでミリ秒(0-999)を取得
int millisecond = now.Millisecond;

// 書式指定子を使用してミリ秒を含む文字列を作成
string formattedTime = now.ToString("yyyy/MM/dd HH:mm:ss.fff");

Console.WriteLine($"現在のミリ秒: {millisecond}");
Console.WriteLine($"フォーマット済み: {formattedTime}");
実行結果
現在のミリ秒: 456
フォーマット済み: 2026/05/15 14:30:05.456

ここで重要なのは、DateTime.Nowの精度はオペレーティングシステムのタイマー解像度に依存するという点です。

Windowsの場合、一般的に15ミリ秒程度の誤差が生じることがあります。

そのため、1ミリ秒単位の極めて厳密な精度が求められる用途には向いていない場合があります。

DateTimeOffsetの推奨

現代的なC#開発では、DateTimeよりもDateTimeOffsetの使用が推奨されることが多いです。

これは、タイムゾーンの情報をより明確に保持できるためです。

ミリ秒の取得方法はDateTimeと同様です。

C#
DateTimeOffset nowOffset = DateTimeOffset.Now;
Console.WriteLine($"ミリ秒(DateTimeOffset): {nowOffset.Millisecond}");

ミリ秒の書式指定(ToString)

ログファイルなどにミリ秒を出力する際、書式指定子の使い方を知っておくと便利です。

C#にはミリ秒を表すためのカスタム書式指定子がいくつか存在します。

「fff」と「FFF」の違い

ミリ秒を表現する際、fff(小文字)とFFF(大文字)では、末尾の「0」の扱いが異なります。

  • fff: 常に3桁で表示します。ミリ秒が10の場合、「010」となります。
  • FFF: 有効数字のみを表示します。末尾が0の場合は省略されます。
C#
DateTime dt = new DateTime(2026, 5, 15, 12, 0, 0, 500);

Console.WriteLine(dt.ToString("ss.fff")); // 出力: 00.500
Console.WriteLine(dt.ToString("ss.FFF")); // 出力: 00.5

データベースへの保存やログのパースを行う場合は、桁数が固定されるfffを使用するのが一般的です。

Stopwatchによる高精度な経過時間の計測

「現在の時刻」ではなく「処理にかかった時間」をミリ秒単位で計測したい場合、DateTime.Nowの差分をとる方法は不適切です。

なぜなら、前述の通りDateTimeの解像度は低く、システム時刻の同期(NTP同期など)によって時刻が逆行したり飛んだりする可能性があるからです。

そこで、経過時間の計測にはSystem.Diagnostics.Stopwatchクラスを使用します。

Stopwatchの基本的な使い方

C#
using System.Diagnostics;

Stopwatch sw = Stopwatch.StartNew();

// 計測対象の重い処理
System.Threading.Thread.Sleep(123); 

sw.Stop();

long elapsedMs = sw.ElapsedMilliseconds;
Console.WriteLine($"経過時間: {elapsedMs}ms");
実行結果
経過時間: 125ms

Stopwatchは、CPUのハードウェアカウンタを利用するため、ナノ秒単位の分解能を持ちます。

ミリ秒単位のパフォーマンス計測においては、これが標準的な手法です。

Stopwatch.GetTimestamp()による低アロケーション計測

高頻度で計測を行うループ内などで、Stopwatchインスタンスを生成(new)したくない場合は、静的メソッドであるGetTimestamp()を使用できます。

C#
long start = Stopwatch.GetTimestamp();

// 何らかの処理
DoSomething();

long end = Stopwatch.GetTimestamp();
TimeSpan elapsed = Stopwatch.GetElapsedTime(start, end);

Console.WriteLine($"計測結果: {elapsed.TotalMilliseconds}ms");

Stopwatch.GetElapsedTimeは.NET 7で導入された非常に便利なメソッドで、アロケーションを抑えつつ正確なTimeSpanを取得できるため、2026年現在のモダンなC#開発では積極的に利用されています。

TimeProviderによるモダンな時刻管理

.NET 8以降、時刻の取得における決定版として導入されたのがTimeProviderクラスです。

これまでのDateTime.NowやStopwatchを直接呼び出す手法には、「ユニットテストが書きにくい」という大きな欠点がありました。

TimeProviderのメリット

TimeProviderを使用すると、本番環境ではシステム時刻を使い、テスト環境では偽の時刻(Mock)を注入することが容易になります。

C#
public class ProcessManager
{
    private readonly TimeProvider _timeProvider;

    public ProcessManager(TimeProvider timeProvider)
    {
        _timeProvider = timeProvider;
    }

    public void LogCurrentMillis()
    {
        DateTimeOffset now = _timeProvider.GetUtcNow();
        Console.WriteLine($"現在時刻(ミリ秒): {now.Millisecond}");
    }
}

TimeProviderでの計測

経過時間を計測する場合も、TimeProvider経由でタイムスタンプを取得できます。

C#
TimeProvider tp = TimeProvider.System;
long start = tp.GetTimestamp();

// 処理実行
Thread.Sleep(50);

long end = tp.GetTimestamp();
double elapsedMs = tp.GetElapsedTime(start, end).TotalMilliseconds;

Console.WriteLine($"TimeProvider計測: {elapsedMs}ms");

このように、TimeProviderはDateTimeとStopwatchの機能を抽象化して統合したような存在です。

2026年現在のエンタープライズ開発において、ミリ秒を扱うコードを書く際は、このTimeProviderを依存注入(DI)して使用することがベストプラクティスとなっています。

ミリ秒計測の使い分け(比較表)

用途に応じてどのクラスを使用すべきか、以下の表にまとめました。

手法主な用途精度テスト容易性
DateTime.Nowログ出力、ユーザーへの時刻表示低(約15ms)低い
Stopwatchローカルなパフォーマンス計測非常に高い低い
TimeProviderモダンな開発、テストが必要な箇所高い非常に高い
Environment.TickCountミリ秒単位の簡易的な経過時間(低負荷)低(約15ms)低い

ミリ秒を扱う際の注意点

C#でミリ秒を扱う上で、初心者が陥りやすい落とし穴がいくつかあります。

1. 精度(Precision)と分解能(Resolution)

「ミリ秒単位で値が取得できること」と「1ミリ秒単位で正確であること」は別問題です。

DateTime.Nowが返す値は1.234秒のようにミリ秒を含みますが、内部の更新間隔が15msであれば、値は1.000, 1.015, 1.030のように飛び飛びになります。

厳密な1msの差異を判定したい場合は、必ずStopwatchまたはTimeProviderを使用してください。

2. 処理自体のオーバーヘッド

ミリ秒単位の計測を行う際、計測コード自体の実行時間が結果に影響を与えることがあります。

特に非常に高速なメソッドを計測する場合、1回の計測ではなく「100万回実行して平均をとる」といったベンチマーク手法が必要です。

このような用途には、自作コードではなくBenchmarkDotNetのようなライブラリの利用を検討してください。

3. DateTime.UtcNowの利用

ミリ秒を扱うのがログ出力や計算目的であれば、DateTime.NowではなくDateTime.UtcNow(またはDateTimeOffset.UtcNow)を使用することを強くお勧めします。

ローカル時刻は夏時間(DST)の切り替わりや時差補正の影響を受け、1ミリ秒を争う計算において予期せぬ不具合を招く可能性があるためです。

まとめ

C#でミリ秒を取得・計測する方法は、近年大きく進化しました。

単に現在のミリ秒を知りたいだけであれば、DateTime.Now.MillisecondやToString("fff")で十分です。

しかし、処理時間を正確に計測してパフォーマンスを最適化したいのであれば、Stopwatchクラスが必須となります。

さらに、保守性の高いコードやテストコードを書く必要がある現代のプロジェクトでは、TimeProviderを活用して時刻管理を抽象化することが標準となっています。

各手法の特性を理解し、用途に合わせて使い分けることで、精度の高い安定したアプリケーションを開発できるようになります。

まずは自身のプロジェクトがどの程度の精度を必要としているかを確認し、最適なクラスを選択することから始めてみてください。