stopwatch.startnew() 是唯一安全的初始化方式,因它内部完成实例化、启动和计数器检测;new stopwatch() 后漏调 start() 会导致 isrunning 为 false、elapsedmilliseconds 恒为 0;测量短操作必须用 elapsed.totalmilliseconds(double,精确到微秒)而非 elapsedmilliseconds(long,截断小数)。

Stopwatch.StartNew() 是唯一安全的初始化方式,其他写法都容易导致 ElapsedMilliseconds 恒为 0。
为什么不能 new Stopwatch() 后再 Start()
手动实例化后漏调 Start() 是最常见错误,尤其在协作修改或重构时——sw 看似已声明,但 IsRunning 始终是 false,所有 Elapsed 属性返回 0。而 Stopwatch.StartNew() 是静态工厂方法,内部完成实例化、启动、高精度计数器可用性检测三件事,开箱即用。
- 错误写法:
var sw = new Stopwatch(); sw.Start();—— 多一步就可能被删掉或注释掉 - 正确写法:
var sw = Stopwatch.StartNew();—— 返回的实例已是运行中状态 - 调试技巧:加一句
Console.WriteLine(sw.IsRunning);,值为True才算真正跑起来了
测量单次短操作必须用 Elapsed.TotalMilliseconds
ElapsedMilliseconds 是 long 类型,只保留毫秒整数部分,0.8ms 的真实耗时会被截断成 0;而 Elapsed.TotalMilliseconds 是 double,能精确到微秒级(例如 0.792341 ms),算法对比、微基准测试(micro-benchmark)中不用它,结果就不可信。
- 适用场景:循环执行 100 次某函数,单次预期约 0.5ms → 必须用
sw.Elapsed.TotalMilliseconds - 格式化建议:日志输出用
$"{sw.Elapsed.TotalMilliseconds:F6}ms",避免科学计数法干扰排查 - 注意浮点误差:做相等判断时别用
==,改用Math.Abs(a - b) 这类阈值比较
重复测量别反复 new,优先复用 + Restart()
频繁调用 Stopwatch.StartNew() 在热路径(如每帧、每请求)中虽不引发 GC(Stopwatch 是 struct),但每次都要查询系统高性能计数器(QueryPerformanceCounter),在虚拟机或旧硬件上开销明显。复用同一实例更稳。
- 循环内测每次迭代:
sw.Restart(); DoWork();—— 等价于Stop()+Start(),且不会抛InvalidOperationException - 分阶段计时(预处理→主逻辑→收尾):
sw.Stop();记录第一段,sw.Restart();开始下一段 - 禁用写法:
sw.Start(); DoWork(); sw.Stop(); sw.Start();—— 第二次Start()会直接抛异常
Stopwatch 不适合 async/await 跨线程计时
Stopwatch 本身不是线程安全的,但真正危险的是 Start() 和 Stop() 被调度到不同线程:await 切换上下文后调用 Stop(),计时器仍按原线程时钟走,结果失真,且不报错。
- 错误模式:
var sw = Stopwatch.StartNew(); await Task.Delay(100); sw.Stop(); - 安全做法:把
Start()和Stop()锁在同一同步上下文内,比如都在一个async方法开头和结尾,中间不跨await - 替代方案:若必须分段测 IO 阶段,改用
Environment.TickCount64差值(注意约 24.9 天溢出),或封装成带捕获线程 ID 的简易计时器
Stopwatch 的精度依赖底层硬件和 OS 支持,但它不关心“什么时候开始”,只回答“跑了多久”。如果你发现两次 Restart() 之间数值跳变剧烈,先检查是否混用了 ElapsedMilliseconds 和 Elapsed.TotalMilliseconds,再确认有没有跨 await 或跨线程误用——这两点最容易被忽略,也最难 debug。











