C# 第一次从 SSD 驱动器顺序读取文本文件非常慢

问题描述 投票:0回答:2

我在 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);

第一次跑的时候我测了一下,第一次说这和文本读取无关,只和文件读取有关:


我对使用 Roslyn 读取文件进行了更多测试

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 会话中第二次和后续时间持续时间相同。

c# .net caching file-read
2个回答
0
投票

如果你需要查明缓存是否真的发生了,你应该绕过缓存读取文件并尝试对其进行基准测试。

在 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


0
投票

你可以避免检查文件长度,减少执行的操作,从而减少加载数据的时间,所以你可以删除这个:

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 的值。如果文件之间的大小差异很大,则可能值得对读取进行排序以将相同存储桶大小的文件组合在一起,因此每个批次“大部分时间”都使用选定的并行化

© www.soinside.com 2019 - 2024. All rights reserved.