效能分析、基準測試、遊戲迴圈、即時系統、科學計算皆需微秒甚至奈秒級計時精度。錯誤的計時方式會導致優化方向錯誤、效能倒退。
| API | 解析度 | 適用場景 | 缺點 |
|---|---|---|---|
Environment.TickCount | ~10-16 ms (系統時脈) | 粗略存活時間 | 精度極低、49 天溢位 |
DateTime.Now | ~10-16 ms | 顯示當前時間 | 系統時鐘精度、受時區/夏令時間影響 |
DateTime.UtcNow | ~10-16 ms | 記錄時間戳 | 同左 |
Stopwatch | 奈秒級 (硬體支援) | 所有高精度測量首選 | 極少數舊硬體無高精度計數器會退回系統時鐘 |
QueryPerformanceCounter (P/Invoke) | 奈秒級 | .NET 1.0/1.1 時代替代品 | 需 P/Invoke、程式碼複雜、現代 .NET 已無必要 |
using System.Diagnostics;
var sw = Stopwatch.StartNew();
// 或
var sw = new Stopwatch();
sw.Start();
// 執行待測程式碼
Thread.Sleep(100); // 模擬工作負載
sw.Stop();
Console.WriteLine($"Elapsed: {sw.Elapsed}"); // TimeSpan
Console.WriteLine($"ElapsedMs: {sw.ElapsedMilliseconds}"); // long 毫秒
Console.WriteLine($"ElapsedTicks: {sw.ElapsedTicks}"); // long 原始刻度
Console.WriteLine($"Frequency: {Stopwatch.Frequency} Hz"); // 每秒刻度數 (唯讀)
var sw = new Stopwatch();
sw.Start();
DoPhase1();
sw.Stop();
Console.WriteLine($"Phase1: {sw.ElapsedMilliseconds} ms");
sw.Restart(); // 歸零並重新開始
DoPhase2();
sw.Stop();
Console.WriteLine($"Phase2: {sw.ElapsedMilliseconds} ms");
// 取得總時間 (需自行累加或用兩個 Stopwatch)
public static double Benchmark(Action action, int warmup = 3, int iterations = 1000)
{
// Warmup (JIT 編譯、快取預熱)
for (int i = 0; i < warmup; i++) action();
// 實測
var sw = new Stopwatch();
long totalTicks = 0;
for (int i = 0; i < iterations; i++)
{
sw.Restart();
action();
sw.Stop();
totalTicks += sw.ElapsedTicks;
}
double avgTicks = (double)totalTicks / iterations;
double avgNanoseconds = avgTicks * (1_000_000_000.0 / Stopwatch.Frequency);
return avgNanoseconds;
}
// 使用範例
double ns = Benchmark(() =>
{
var sum = 0;
for (int i = 0; i < 1000; i++) sum += i;
}, iterations: 10000);
Console.WriteLine($"Average: {ns:F2} ns");
.NET Framework 1.0/1.1 缺乏 Stopwatch,開發者必須 P/Invoke Win32 API:
using System;
using System.Runtime.InteropServices;
public class HighResolutionTimer // .NET 1.x 時代產物
{
private long _start, _stop, _frequency;
public HighResolutionTimer()
{
QueryPerformanceFrequency(ref _frequency);
}
public void Start() => QueryPerformanceCounter(ref _start);
public void Stop() => QueryPerformanceCounter(ref _stop);
public double ElapsedSeconds => (double)(_stop - _start) / _frequency;
[DllImport("KERNEL32.dll", CharSet = CharSet.Auto)]
private static extern bool QueryPerformanceCounter(ref long lpPerformanceCount);
[DllImport("KERNEL32.dll", CharSet = CharSet.Auto)]
private static extern bool QueryPerformanceFrequency(ref long lpFrequency);
}
Stopwatch 內部已封裝 QueryPerformanceCounter,並處理跨處理器核心同步、電源管理頻率變動等邊緣情況。| 計時源 | 頻率 | 特性 | 現代作業系統策略 |
|---|---|---|---|
| TSC (Time Stamp Counter) | CPU 主頻 (GHz 級) | 極高精度、核心間可能不同步、變頻影響 | Invariant TSC (恆定頻率) + 核心同步修正 |
| HPET (High Precision Event Timer) | 10-100 MHz | 獨立晶振、精度穩定、延遲較高 | 備用計時源 |
| ACPI PM Timer | 3.58 MHz | 舊標準、精度一般 | 相容性備用 |
Windows 作業系統抽象層 (HAL) 自動選擇最佳可用計時源,Stopwatch.Frequency 反映實際使用的硬體頻率。
| 陷阱 | 正確做法 |
|---|---|
用 DateTime.Now 測效能 | 永遠用 Stopwatch |
| 單次測量得出結論 | 最少 1000 次迭代 + Warmup + 統計分佈 (中位數/百分位數) |
| 忽略 JIT 編譯時間 | 測試前先執行數次 Warmup |
| 測試極短操作 (< 100 ns) | 使用迴路放大測量,除以迴圈次數 |
| 多執行緒干擾 | 設定 Process.GetCurrentProcess().ProcessorAffinity、Thread.Priority = ThreadPriority.Highest |
| GC 干擾 | 測試前 GC.Collect()、GC.WaitForPendingFinalizers()、GC.TryStartNoGCRegion() (.NET 4.6+) |
// 安裝 NuGet: BenchmarkDotNet
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
[MemoryDiagnoser]
public class StringConcatBenchmark
{
[Benchmark(Baseline = true)]
public string StringConcat() => "a" + "b" + "c";
[Benchmark]
public string StringBuilder() => new StringBuilder().Append("a").Append("b").Append("c").ToString();
[Benchmark]
public string Interpolation() => $"{"a"}{"b"}{"c"}";
}
public class Program
{
public static void Main() => BenchmarkRunner.Run();
}
BenchmarkDotNet 自動處理:Warmup、迭代次數動態調整、統計分析、記憶體分配統計、結果匯出 (Markdown/HTML/CSV)、環境資訊記錄。
QueryPerformanceCounter 屬於 .NET 1.x 歷史遺產,現代專案嚴禁使用。