stopwatch.startnew() 是唯一安全的初始化方式,因其内部完成实例化、启动及计数器检测,确保返回即运行中状态;手动 new 后漏调 start() 会导致 isrunning 为 false、所有 elapsed 属性恒为 0。

Stopwatch.StartNew() 是唯一安全的初始化方式,其他写法在生产环境里大概率导致 ElapsedMilliseconds 恒为 0。
为什么 Stopwatch.StartNew() 是强制推荐的写法
手动 new Stopwatch() 后再调用 Start() 多了一步,协作或重构时容易被删掉、注释掉,甚至被 IDE 自动格式化“优化”掉。一旦漏掉 Start(),sw.IsRunning 就一直是 false,所有 Elapsed* 属性都返回 0 —— 你代码明明跑了,日志却显示“耗时 0ms”。Stopwatch.StartNew() 是静态工厂方法,内部完成实例化 + 启动 + 高精度计数器可用性检测,返回即运行中状态,语义明确、无遗漏风险。
常见错误现象:
-
sw.ElapsedMilliseconds总是 0,但Console.WriteLine(sw.IsRunning)输出False - 调试时发现
sw.ElapsedTicks始终没变,说明计时根本没启动
ElapsedMilliseconds 和 Elapsed.TotalMilliseconds 别混用
这两个属性名字像,行为完全不同:ElapsedMilliseconds 是 long 类型,只保留毫秒整数部分,小数全部截断(不是四舍五入);Elapsed.TotalMilliseconds 是 double,带完整小数精度,能反映真实微秒级差异。
适用场景:
- 测单次短操作(比如一次
Dictionary.TryGetValue或字符串拼接),预期耗时 sw.Elapsed.TotalMilliseconds,否则全显示为 0 - 做平均值计算(如循环 1000 次取均值)→ 用
TotalMilliseconds,避免整数截断引入系统性偏差 - 仅需粗略提示(如 UI 上显示“响应时间:23ms”)→
ElapsedMilliseconds足够,省去格式化小数的麻烦
输出建议:$"{sw.Elapsed.TotalMilliseconds:F6}ms",避免科学计数法干扰排查。
重复测量别反复 new,优先复用 + Restart()
Stopwatch 是 struct,栈上分配,不触发 GC,但 Stopwatch.StartNew() 每次都会调用底层 QueryPerformanceCounter 查询硬件计数器,在虚拟机、老旧 BIOS 或省电模式下开销明显。热路径(如每帧、每请求)中应复用同一实例。
正确做法:
- 循环内测每次迭代:
sw.Restart(); DoWork();—— 等价于Stop()+Reset()+Start(),安全且语义清晰 - 分阶段计时(预处理→主逻辑→收尾):
sw.Stop();记录第一段,sw.Restart();开始下一段 - 禁用写法:
sw.Start(); DoWork(); sw.Stop(); sw.Start();—— 第二次Start()会抛InvalidOperationException:“Cannot start a stopped stopwatch”
注意:Reset() 不会停止计时器,它只是把累计时间清零,而计时仍在继续 —— 这个行为极易误用。
精度不是默认就有,得看 Stopwatch.IsHighResolution
Stopwatch.IsHighResolution 返回 true 才表示当前使用 CPU 周期级计数器(精度通常 ~100ns);返回 false 则退化到系统时钟(Windows 上常为 10–15ms 分辨率),此时测任何低于 20ms 的操作都没意义。
这不是理论问题,而是真实限制:
- 某些虚拟机、旧版 BIOS、Windows 省电策略会直接禁用高精度计数器 “
- 真正在意性能数字时,得手动跑预热循环、强制 GC、关闭后台任务,否则
Stopwatch再准也只是给噪声计时
IsHighResolution == true” 只说明硬件支持,不代表你的测量结果就可信——JIT 预热、GC 干扰、CPU 频率波动都会污染数据
别把 Stopwatch 当成黑盒秒表,它只回答“这段代码跑了多久”,不保证这个“多久”是你真正想问的那个“多久”。











