我在 Windows 会话期间第一次从 SSD 连续读取文本文件时遇到非常缓慢的情况。然后第二次和连续读取速度超过 60 倍(在应用程序的第二次和后续运行中,这意味着不同的 Windows 进程)。我很确定这是系统和/或 SSD 缓存优化的正常结果,但我在这里提出一个问题以与社区仔细检查这个猜测,因为我没有通过谷歌搜索找到任何东西。
问题:可以做些什么让第一次阅读更快吗?
详情:
我在两台不同的机器上重现了这个问题,一台配备三星固态硬盘 MZVLB1T0HALR 1TB(最大读取速度 3200MB/秒)一台配备三星固态硬盘 MZVL21T0HCLR 1TB(最大读取速度 7000MB/秒)。
通过经典的
File.ReadAllText(filePathStr)
调用,我第一次获得 4.737 个文件的 33.555 秒(来自 dotTrace 的屏幕截图)。所有 4.7K 文本文件在磁盘上的总重量为 28MB,这低于 1MB 读取/秒。
然后第二次和随后的时间它快了 60 倍以上,540 毫秒而不是 33 秒,大约 60MB 读取/秒(与宣布的 SSD 最大读取速度 3200MB/秒仍然相去甚远,但我们读取了 4.7K 个文件而不是一个)。
我知道
File.ReadAllText()
有一些限制(比如假设文本文件是 UTF8 编码的,这在我们的上下文中是可以的)所以我尝试了类似的东西:
FileInfo fileInfo = new FileInfo(filePathStr);
long length = fileInfo.Length;
int bufferSize = length >= int.MaxValue ? int.MaxValue : (int)length;
using (FileStream fs = File.Open(filePathStr, FileMode.Open, FileAccess.Read))
using (StreamReader streamReader = new StreamReader(
fs, Encoding.UTF8, true, bufferSize, false)) {
content = streamReader.ReadToEnd();
failureReason = null;
return true;
}
...第一次具有同样超慢的性能,第二次和随后的性能更慢(853 毫秒而不是 540 毫秒):
最后我用这段代码做了一个测试:
byte[] bytes = File.ReadAllBytes(filePathStr);
string content = File.ReadAllText(filePathStr);
第一次跑的时候我测了一下,第一次说这和文本读取无关,只和文件读取有关:
SourceReferenceResolver.ReadText(string resolvedPath):SourceText
并得到了完全相同的结果:读取 4.700 个源文件时速度慢 60 倍到 80 倍,具体取决于它是在 Windows 启动后第一次执行还是第二次或后续时间执行(从 23 秒到在强大的新机器上 270 毫秒!)。我想如果有一种方法可以实现更好的性能 Roslyn 开发人员会实现它,因为他们都是专家并且非常关心性能。
有人建议去异步,所以我尝试了类似的东西:
var tasks = new List<Task<string>>();
foreach (string path in paths) {
var task = File.ReadAllTextAsync(path);
tasks.Add(task);
}
foreach(var task in tasks) {
string str = await task;
}
但结果与使用
File.ReadAllText(path);
相同:Wnd 会话中第一次持续时间相同,Wnd 会话中第二次和后续时间持续时间相同。
如果你需要查明缓存是否真的发生了,你应该绕过缓存读取文件并尝试对其进行基准测试。
在 Windows 上,您可以通过在流构造函数的 FileOptions 选项参数中传递 FILE_FLAG_NO_BUFFERING 标志来禁用缓冲。 FileOptions 枚举中没有这样的值,您必须传递它的数值(0x2000000 - 该值在 FileStream 源代码中找到并被视为有效)。
FileStream readStream = new FileStream(
path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, buffer, (FileOptions)0x20000000);
http://saplin.blogspot.com/2018/07/non-cachedunbuffered-file-operations.html
你可以避免检查文件长度,减少执行的操作,从而减少加载数据的时间,所以你可以删除这个:
FileInfo fileInfo = new FileInfo(filePathStr);
long length = fileInfo.Length;
int bufferSize = length >= int.MaxValue ? int.MaxValue : (int)length;
对 bufferSize 的“合理”可配置值进行试验是有意义的(这样你就可以为每台机器调整它)直到找到最佳位置。
括号:根据我的理解,bufferSize 确实应该只与机器相关,并且只会影响大于 bufferSize 的文件。当文件较小时它变红到最后然后它必须被推入内存所以当文件“小”时将 bufferSize 设置为等于文件长度没有性能优势,在我看来你没有严格准备好分配 int.MaxValue 作为缓冲区时的内存限制。另一方面,对于“更大”的文件,会有“很多”磁盘到内存的写入,但最适应的块大小与内存总线的大小和磁盘特性有关。然而,我不仅需要用我的推理来支持这一点。然而,之前的建议仍然有意义,因为您会减少 I/O。
你也可以尝试并行化你的代码,如果还没有的话,在这两种情况下都要注意使用 ReadToEndAsync 切换到异步模式,以更好地利用 OS/Machine 进行并行 I/O 操作,并找到甜蜜的指定磁盘/机器可以处理的并行读取数。
var numParallelReads = 5;
var bufferSize = 4096;
var groupedPaths = paths.Select((path,idx) => (path, idx)).GroupBy(h => h.idx/numParallelReads);
foreach (var group in groupedPaths)
{
var parallelPaths = group.Select(x => x.path);
var tasks = parallelPaths.Select(p => {
using var sr = new StreamReader(p, Encoding.UTF8, true, bufferSize);
return sr.ReadToEndAsync();
});
await Task.WhenAll(tasks);
}
调整 numParallelReads 和 bufferSize 的值。如果文件之间的大小差异很大,则可能值得对读取进行排序以将相同存储桶大小的文件组合在一起,因此每个批次“大部分时间”都使用选定的并行化