必须用Stopwatch测文件操作耗时,因其基于高精度计数器;大文件读应增大缓冲区;避免嵌套流和高频Path.GetFullPath;用PerfMon查磁盘真实瓶颈;防病毒软件可能显著拖慢IO。

用 Stopwatch 精确测量单次文件操作耗时
直接看 DateTime.Now 或 DateTime.UtcNow 容易受系统时钟调整、NTP同步干扰,测不准毫秒级以下差异。必须用 Stopwatch——它基于高精度性能计数器,是 .NET 中唯一推荐的微秒级计时方式。
实操建议:
- 在关键路径(如
File.ReadAllBytes、FileStream.Read、Directory.GetFiles)前后调用Stopwatch.Start()和Stopwatch.ElapsedMilliseconds - 避免在循环内反复创建
Stopwatch实例,复用一个实例并调用Restart() - 注意:仅测“耗时”不能定位瓶颈类型(是磁盘寻道?缓存未命中?还是锁竞争?),但它能快速排除“是不是这段代码本身太慢”
检查是否被 FileStream 缓冲策略拖慢
默认构造的 FileStream 启用 4KB 缓冲,对小文件读写友好,但对大文件连续读(如视频、日志归档)可能因频繁 flush 或缓冲区拷贝反而变慢;而禁用缓冲(useAsync: false, bufferSize: 1)又会放大系统调用开销。
常见错误现象:
- 读取 100MB 文件耗时远超预期,且 CPU 占用低、磁盘队列长度高 → 很可能是缓冲区太小导致 syscall 过多
- 写入大量小文件时内存占用飙升 → 缓冲区累积未刷盘,或
StreamWriter套在FileStream上造成双重缓冲
实操建议:
- 大文件顺序读:显式指定较大缓冲区(如
new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 64 * 1024)) - 避免嵌套流:不用
new StreamReader(new FileStream(...)),改用File.OpenRead()+ 手动缓冲,或直接用Span+FileStream.ReadExactly(.NET 5+) - 写入后及时调用
Flush()或设leaveOpen: false,防止句柄泄漏拖慢后续 IO
用 Windows 性能监视器(PerfMon)验证底层磁盘压力
.NET 层面测出慢,不等于硬盘真慢。可能是其他进程占满 IOPS、NTFS 日志满、卷影复制(VSS)正在备份、甚至 SSD 寿命告警触发限速。光看 C# 代码没用,得看系统级指标。
关键计数器(添加到 PerfMon):
-
PhysicalDisk\Avg. Disk sec/Read> 25ms 或Avg. Disk sec/Write> 10ms → 磁盘响应延迟异常 -
PhysicalDisk\% Disk Time持续 > 90% → 磁盘饱和,需查是哪个进程在刷盘(用Resource Monitor的 Disk 标签页) -
LogicalDisk\Split IO/sec高 → 文件碎片严重,尤其影响机械盘随机读
注意:File.Copy 或 Directory.Move 在同卷内实际是 NTFS 重命名(瞬间完成),但如果跨卷、或目标盘有防病毒软件实时扫描,就会退化为全量拷贝——这时 PerfMon 会看到持续的 Write/sec 和 Disk Bytes/sec 上升。
警惕 Path.GetFullPath 和 Directory.EnumerateFiles 的隐藏开销
这两个方法看着轻量,但在某些场景下会触发大量系统调用和 UNC 路径解析,成为隐形瓶颈。
典型问题:
-
Path.GetFullPath(@"..\config\app.json")在 Web 应用中高频调用 → 每次都做当前工作目录拼接 + 遍历父目录检查是否存在,Windows 下还涉及符号链接解析 -
Directory.EnumerateFiles(@"C:\Logs", "*.log", SearchOption.AllDirectories)遇到挂载的网络驱动器或损坏的 junction point → 可能卡住数秒甚至抛UnauthorizedAccessException,且无法中断
实操建议:
- 路径拼接尽量用
Path.Combine,避免运行时反复调用GetFullPath;启动时解析一次,缓存绝对路径 - 枚举文件前先用
Directory.Exists快速失败;必要时用FindFirstFileExP/Invoke 实现带超时的枚举(.NET 6+ 可考虑FileSystemEnumerable) - 禁用不必要的搜索选项:不用
AllDirectories就别用;通配符尽量具体("error_*.log"比"*.log"快)
最常被忽略的是防病毒软件实时扫描——它会在每个 CreateFile 调用上拦截,尤其是打开/写入配置文件、临时文件时。临时关闭 AV 测试对比,能立刻确认是否中招。











