C#I / O Parallelism确实提高了SSD的性能?

我在这里读了一些答案( 例如 ),其中一些人说并行性不会提高性能(可能在读取IO中)。

但我创建了一些测试,表明WRITE操作也更快。

阅读测试:

我用伪数据创建了随机6000文件:

在此处输入图像描述

让我们尝试用w / o并行性来阅读它们:

var files = Directory.GetFiles("c:\\temp\\2\\", "*.*", SearchOption.TopDirectoryOnly).Take(1000).ToList(); var sw = Stopwatch.StartNew(); files.ForEach(f => ReadAllBytes(f).GetHashCode()); sw.ElapsedMilliseconds.Dump("Run READ- Serial"); sw.Stop(); sw.Restart(); files.AsParallel().ForAll(f => ReadAllBytes(f).GetHashCode()); sw.ElapsedMilliseconds.Dump("Run READ- Parallel"); sw.Stop(); 

结果1:

运行READ- Serial 595

运行READ- Parallel 193

结果2:

运行READ- Serial 316

运行READ-Parallel 192

写测试:

要创建1000个随机文件,每个文件为300K。 (我从prev test中清空了目录)

在此处输入图像描述

 var bytes = new byte[300000]; Random r = new Random(); r.NextBytes(bytes); var list = Enumerable.Range(1, 1000).ToList(); sw.Restart(); list.ForEach((f) => WriteAllBytes(@"c:\\temp\\2\\" + Path.GetRandomFileName(), bytes)); sw.ElapsedMilliseconds.Dump("Run WRITE serial"); sw.Stop(); sw.Restart(); list.AsParallel().ForAll((f) => WriteAllBytes(@"c:\\temp\\2\\" + Path.GetRandomFileName(), bytes)); sw.ElapsedMilliseconds.Dump("Run WRITE Parallel"); sw.Stop(); 

结果1:

运行WRITE serial 2028

运行WRITE Parallel 368

结果2:

运行WRITE serial 784

运行WRITE Parallel 426

题:

结果让我感到惊讶。 很明显,出乎所有的期望(特别是对于WRITE操作) – 并行性和IO操作的性能更好。

如何/为什么并行性结果更好? 似乎SSD可以与线程一起使用,并且在IO设备中一次运行多个作业时没有/更少的瓶颈。

Nb我没有用硬盘测试它(我很高兴有硬盘驱动器会运行测试。)

基准测试是一项棘手的艺术,你只是没有衡量你的想法。 从测试结果来看,它实际上并不是I / O开销,为什么单线程代码在第二次运行时会更快?

您不指望的是文件系统缓存的行为。 它在磁盘中保留磁盘内容的副本。 这对multithreading代码测量有特别大的影响,它根本不使用任何I / O. 简而言之:

  • 如果文件系统缓存具有数据副本,则读取来自RAM。 它以内存总线速度运行,通常约为35千兆字节/秒。 如果它没有副本,则读取将延迟,直到磁盘提供数据。 它不仅可以读取请求的群集,还可以读取整个磁盘上的数据。

  • 写入直接到RAM,很快完成。 当程序继续执行时,该数据在后台懒惰地写入磁盘,优化以最小化磁道顺序中的写头移动。 只有当没有更多的RAM可用时,写入才会停止。

实际高速缓存大小取决于安装的RAM量以及运行进程对RAM的需求。 一个非常粗略的指导原则是,你可以在拥有4GB内存的机器上使用1GB,在拥有8GB内存的机器上使用3GB。 它在资源监视器,内存选项卡中可见,显示为“缓存”值。 请记住,它是高度可变的。

因此,足以理解您所看到的内容,并行测试从串行测试中获益很大,已经读取了所有数据。 如果您已经编写了测试以便首先运行Parallel测试,那么您将得到非常不同的结果。 只有当缓存是冷的时候才能看到由于线程导致的性能损失。 您必须重新启动计算机才能确保满足此条件。 或者首先读取另一个非常大的文件,大到足以驱逐缓存中的有用数据。

只有当您对程序有先验知识时,才能读取刚刚写入的数据,您可以安全地使用线程而不会有丢失的风险。 这种保证通常很难得到。 它确实存在,一个很好的例子是Visual Studio构建您的项目。 编译器将构建结果写入obj \ Debug目录,然后MSBuild将其复制到bin \ Debug。 看起来非常浪费,但事实并非如此,因为文件在缓存中很热,所以复制将始终很快完成。 缓存还解释了.NET程序的冷启动和热启动之间的区别以及为什么使用NGen并不总是最好的。

此行为的原因称为文件缓存 ,它是一种Windowsfunction,可提高文件操作的性能。 我们来看一下Windows开发中心的简短说明:

默认情况下,Windows会缓存从磁盘读取并写入磁盘的文件数据。 这意味着读取操作从系统内存中称为系统文件缓存的区域读取文件数据,而不是从物理磁盘读取。

这意味着硬盘(通常)在测试期间从未使用过。

我们可以通过使用MSDN中记录的FILE_FLAG_NO_BUFFERING标志创建FileStream来避免此行为。 让我们看一下使用这个标志的新的ReadUnBuffered函数:

 private static object ReadUnbuffered(string f) { //Unbuffered read and write operations can only //be performed with blocks having a multiple //size of the hard drive sector size byte[] buffer = new byte[4096 * 10]; const ulong FILE_FLAG_NO_BUFFERING = 0x20000000; using (FileStream fs = new FileStream( f, FileMode.Open, FileAccess.Read, FileShare.None, 8, (FileOptions)FILE_FLAG_NO_BUFFERING)) { return fs.Read(buffer, 0, buffer.Length); } } 

结果:读串口要快得多。 在我的情况下甚至快两倍。

使用标准Windows缓存读取文件只需要执行CPU和RAM操作来管理文件的链接,处理FileStream ,…因为文件已经被缓存。 当然,它不是CPU密集型的,但它不可忽略不计。 由于文件已经在系统缓存中,因此并行方法(没有缓存修改)准确显示了这些开销操作的时间。

此行为也可以转移到写入操作。

这是一个非常有趣的话题! 对不起,我无法解释技术细节,但有一些问题需要提出。 它有点长,所以我无法将它们纳入评论。 请原谅我把它作为“答案”发布。

我认为你需要考虑大文件和小文件,同样,测试必须运行几次并获得平均时间以确保结果是可validation的。 一般指导原则是在进化计算中提出25次。

另一个问题是关于系统缓存。 你只创建了一个bytes缓冲区并且总是写同样的东西,我不知道系统如何处理缓冲区,但为了最小化差异,我建议你为不同的文件创建不同的缓冲区。

(更新:也许GC也会影响性能,所以我再次修改,尽可能地将GC放在一边。)

我幸运地在我的计算机上安装了SSD和HDD,并修改了测试代码。 我用不同的配置执行它并获得以下结果。 希望我可以激励某人寻求更好的解释。

1KB,256个文件

 Avg Write Parallel SSD: 46.88 Avg Write Serial SSD: 94.32 Avg Read Parallel SSD: 4.28 Avg Read Serial SSD: 15.48 Avg Write Parallel HDD: 35.4 Avg Write Serial HDD: 71.52 Avg Read Parallel HDD: 4.52 Avg Read Serial HDD: 14.68 

512KB,256个文件

 Avg Write Parallel SSD: 86.84 Avg Write Serial SSD: 210.84 Avg Read Parallel SSD: 65.64 Avg Read Serial SSD: 80.84 Avg Write Parallel HDD: 85.52 Avg Write Serial HDD: 186.76 Avg Read Parallel HDD: 63.24 Avg Read Serial HDD: 82.12 // Note: GC seems still kicked in the parallel reads on this test 

我的机器是:i7-6820HQ / 32G / Windows 7 Enterprise x64 / VS2017 Professional / Target .NET 4.6 /在调试模式下运行。

这两个硬盘是:

C盘:IDE \ Crucial_CT275MX300SSD4 ___________________ M0CR021

D盘:IDE \ ST2000LM003_HN-M201RAD __________________ 2BE10001

修订后的代码如下:

 Stopwatch sw = new Stopwatch(); string path; int fileSize = 1024 * 1024 * 1024; int numFiles = 2; byte[] bytes = new byte[fileSize]; Random r = new Random(DateTimeOffset.UtcNow.Millisecond); List list = Enumerable.Range(0, numFiles).ToList(); List> allBytes = new List>(numFiles); List files; int numTests = 1; List wss = new List(numTests); List wps = new List(numTests); List rss = new List(numTests); List rps = new List(numTests); List wsh = new List(numTests); List wph = new List(numTests); List rsh = new List(numTests); List rph = new List(numTests); Enumerable.Range(1, numTests).ToList().ForEach((i) => { path = @"C:\SeqParTest\"; allBytes.Clear(); GC.Collect(); GC.WaitForFullGCComplete(); list.ForEach((x) => { r.NextBytes(bytes); allBytes.Add(new List(bytes)); }); try { GC.TryStartNoGCRegion(0, true); } catch (Exception) { } sw.Restart(); list.AsParallel().ForAll((x) => File.WriteAllBytes(path + Path.GetRandomFileName(), allBytes[x].ToArray())); wps.Add(sw.ElapsedMilliseconds); sw.Stop(); try { GC.EndNoGCRegion(); } catch (Exception) { } Debug.Print($"Write parallel SSD #{i}: {wps[i - 1]}"); allBytes.Clear(); GC.Collect(); GC.WaitForFullGCComplete(); list.ForEach((x) => { r.NextBytes(bytes); allBytes.Add(new List(bytes)); }); try { GC.TryStartNoGCRegion(0, true); } catch (Exception) { } sw.Restart(); list.ForEach((x) => File.WriteAllBytes(path + Path.GetRandomFileName(), allBytes[x].ToArray())); wss.Add(sw.ElapsedMilliseconds); sw.Stop(); try { GC.EndNoGCRegion(); } catch (Exception) { } Debug.Print($"Write serial SSD #{i}: {wss[i - 1]}"); files = Directory.GetFiles(path, "*.*", SearchOption.TopDirectoryOnly).Take(numFiles).ToList(); try { GC.TryStartNoGCRegion(0, true); } catch (Exception) { } sw.Restart(); files.AsParallel().ForAll(f => File.ReadAllBytes(f).GetHashCode()); rps.Add(sw.ElapsedMilliseconds); sw.Stop(); try { GC.EndNoGCRegion(); } catch (Exception) { } files.ForEach(f => File.Delete(f)); Debug.Print($"Read parallel SSD #{i}: {rps[i - 1]}"); GC.Collect(); GC.WaitForFullGCComplete(); files = Directory.GetFiles(path, "*.*", SearchOption.TopDirectoryOnly).Take(numFiles).ToList(); try { GC.TryStartNoGCRegion(0, true); } catch (Exception) { } sw.Restart(); files.ForEach(f => File.ReadAllBytes(f).GetHashCode()); rss.Add(sw.ElapsedMilliseconds); sw.Stop(); try { GC.EndNoGCRegion(); } catch (Exception) { } files.ForEach(f => File.Delete(f)); Debug.Print($"Read serial SSD #{i}: {rss[i - 1]}"); GC.Collect(); GC.WaitForFullGCComplete(); path = @"D:\SeqParTest\"; allBytes.Clear(); GC.Collect(); GC.WaitForFullGCComplete(); list.ForEach((x) => { r.NextBytes(bytes); allBytes.Add(new List(bytes)); }); try { GC.TryStartNoGCRegion(0, true); } catch (Exception) { } sw.Restart(); list.AsParallel().ForAll((x) => File.WriteAllBytes(path + Path.GetRandomFileName(), allBytes[x].ToArray())); wph.Add(sw.ElapsedMilliseconds); sw.Stop(); try { GC.EndNoGCRegion(); } catch (Exception) { } Debug.Print($"Write parallel HDD #{i}: {wph[i - 1]}"); allBytes.Clear(); GC.Collect(); GC.WaitForFullGCComplete(); list.ForEach((x) => { r.NextBytes(bytes); allBytes.Add(new List(bytes)); }); try { GC.TryStartNoGCRegion(0, true); } catch (Exception) { } sw.Restart(); list.ForEach((x) => File.WriteAllBytes(path + Path.GetRandomFileName(), allBytes[x].ToArray())); wsh.Add(sw.ElapsedMilliseconds); sw.Stop(); try { GC.EndNoGCRegion(); } catch (Exception) { } Debug.Print($"Write serial HDD #{i}: {wsh[i - 1]}"); files = Directory.GetFiles(path, "*.*", SearchOption.TopDirectoryOnly).Take(numFiles).ToList(); try { GC.TryStartNoGCRegion(0, true); } catch (Exception) { } sw.Restart(); files.AsParallel().ForAll(f => File.ReadAllBytes(f).GetHashCode()); rph.Add(sw.ElapsedMilliseconds); sw.Stop(); try { GC.EndNoGCRegion(); } catch (Exception) { } files.ForEach(f => File.Delete(f)); Debug.Print($"Read parallel HDD #{i}: {rph[i - 1]}"); GC.Collect(); GC.WaitForFullGCComplete(); files = Directory.GetFiles(path, "*.*", SearchOption.TopDirectoryOnly).Take(numFiles).ToList(); try { GC.TryStartNoGCRegion(0, true); } catch (Exception) { } sw.Restart(); files.ForEach(f => File.ReadAllBytes(f).GetHashCode()); rsh.Add(sw.ElapsedMilliseconds); sw.Stop(); try { GC.EndNoGCRegion(); } catch (Exception) { } files.ForEach(f => File.Delete(f)); Debug.Print($"Read serial HDD #{i}: {rsh[i - 1]}"); GC.Collect(); GC.WaitForFullGCComplete(); }); Debug.Print($"Avg Write Parallel SSD: {wps.Average()}"); Debug.Print($"Avg Write Serial SSD: {wss.Average()}"); Debug.Print($"Avg Read Parallel SSD: {rps.Average()}"); Debug.Print($"Avg Read Serial SSD: {rss.Average()}"); Debug.Print($"Avg Write Parallel HDD: {wph.Average()}"); Debug.Print($"Avg Write Serial HDD: {wsh.Average()}"); Debug.Print($"Avg Read Parallel HDD: {rph.Average()}"); Debug.Print($"Avg Read Serial HDD: {rsh.Average()}"); 

好吧,我还没有完全测试代码,所以它可能有问题。 我意识到它有时会停止并行读取,我认为这是因为在下一步读取现有文件列表后,顺序读取文件的删除已完成,因此它会报告文件未找到错误。

另一个问题是我使用新创建的文件进行读取测试。 从理论上讲,最好不要这样做(甚至重新启动计算机/填充SSD上的空白区域以避免缓存),但我没有打扰,因为预期的比较是在顺序和并行性能之间。

更新:

我不知道如何解释原因,但我认为可能是因为IO资源非常闲置? 接下来我会尝试两件事:

  1. 串行/并行的大文件(1GB)
  2. 当其他后台活动使用磁盘IO时。

更新2:

大文件(512M,32个文件)的一些结果:

 Write parallel SSD #1: 140935 Write serial SSD #1: 133656 Read parallel SSD #1: 62150 Read serial SSD #1: 43355 Write parallel HDD #1: 172448 Write serial HDD #1: 138381 Read parallel HDD #1: 173436 Read serial HDD #1: 142248 Write parallel SSD #2: 122286 Write serial SSD #2: 119564 Read parallel SSD #2: 53227 Read serial SSD #2: 43022 Write parallel HDD #2: 175922 Write serial HDD #2: 137572 Read parallel HDD #2: 204972 Read serial HDD #2: 142174 Write parallel SSD #3: 121700 Write serial SSD #3: 117730 Read parallel SSD #3: 107546 Read serial SSD #3: 42872 Write parallel HDD #3: 171914 Write serial HDD #3: 145923 Read parallel HDD #3: 193097 Read serial HDD #3: 142211 Write parallel SSD #4: 125805 Write serial SSD #4: 118252 Read parallel SSD #4: 113385 Read serial SSD #4: 42951 Write parallel HDD #4: 176920 Write serial HDD #4: 137520 Read parallel HDD #4: 208123 Read serial HDD #4: 142273 Write parallel SSD #5: 116394 Write serial SSD #5: 116592 Read parallel SSD #5: 61273 Read serial SSD #5: 43315 Write parallel HDD #5: 172259 Write serial HDD #5: 138554 Read parallel HDD #5: 275791 Read serial HDD #5: 142311 Write parallel SSD #6: 107839 Write serial SSD #6: 135071 Read parallel SSD #6: 79846 Read serial SSD #6: 43328 Write parallel HDD #6: 176034 Write serial HDD #6: 138671 Read parallel HDD #6: 218533 Read serial HDD #6: 142481 Write parallel SSD #7: 120438 Write serial SSD #7: 118032 Read parallel SSD #7: 45375 Read serial SSD #7: 42978 Write parallel HDD #7: 173151 Write serial HDD #7: 140579 Read parallel HDD #7: 176492 Read serial HDD #7: 142153 Write parallel SSD #8: 108862 Write serial SSD #8: 123556 Read parallel SSD #8: 120162 Read serial SSD #8: 42983 Write parallel HDD #8: 174699 Write serial HDD #8: 137619 Read parallel HDD #8: 204069 Read serial HDD #8: 142480 Write parallel SSD #9: 111618 Write serial SSD #9: 117854 Read parallel SSD #9: 51224 Read serial SSD #9: 42970 Write parallel HDD #9: 173069 Write serial HDD #9: 136936 Read parallel HDD #9: 159978 Read serial HDD #9: 143401 Write parallel SSD #10: 115381 Write serial SSD #10: 118545 Read parallel SSD #10: 79509 Read serial SSD #10: 43818 Write parallel HDD #10: 179545 Write serial HDD #10: 138556 Read parallel HDD #10: 167978 Read serial HDD #10: 143033 Write parallel SSD #11: 113105 Write serial SSD #11: 116849 Read parallel SSD #11: 84309 Read serial SSD #11: 42620 Write parallel HDD #11: 179432 Write serial HDD #11: 139014 Read parallel HDD #11: 219161 Read serial HDD #11: 142515 Write parallel SSD #12: 124901 Write serial SSD #12: 121769 Read parallel SSD #12: 137192 Read serial SSD #12: 43144 Write parallel HDD #12: 176091 Write serial HDD #12: 139042 Read parallel HDD #12: 214205 Read serial HDD #12: 142576 Write parallel SSD #13: 110896 Write serial SSD #13: 123152 Read parallel SSD #13: 56633 Read serial SSD #13: 42665 Write parallel HDD #13: 173123 Write serial HDD #13: 138514 Read parallel HDD #13: 210003 Read serial HDD #13: 142215 Write parallel SSD #14: 117762 Write serial SSD #14: 126865 Read parallel SSD #14: 90005 Read serial SSD #14: 44089 Write parallel HDD #14: 172958 Write serial HDD #14: 139908 Read parallel HDD #14: 217826 Read serial HDD #14: 142216 Write parallel SSD #15: 109912 Write serial SSD #15: 121276 Read parallel SSD #15: 72285 Read serial SSD #15: 42827 Write parallel HDD #15: 176255 Write serial HDD #15: 139084 Read parallel HDD #15: 183926 Read serial HDD #15: 142111 Write parallel SSD #16: 122476 Write serial SSD #16: 126283 Read parallel SSD #16: 47875 Read serial SSD #16: 43799 Write parallel HDD #16: 173436 Write serial HDD #16: 137203 Read parallel HDD #16: 294374 Read serial HDD #16: 142387 Write parallel SSD #17: 112168 Write serial SSD #17: 121079 Read parallel SSD #17: 79001 Read serial SSD #17: 43207 

我很遗憾没有时间完成所有25次运行,但结果显示在大型文件中,如果磁盘使用率已满,顺序R / W可能比并行快。 我认为这可能是其他关于SO的讨论的原因。

首先,测试需要排除任何CPU / RAM操作(GetHashCode),因为在执行下一个磁盘操作之前串行代码可能正在等待CPU。

在内部,SSD总是试图平衡其不同内部芯片之间的操作。 它的能力取决于模型,它有多少(TRIMmed)自由空间等。直到前一段时间,这应该在parallell和serial中表现相同,因为OS和SSD之间的队列无论如何都是串行的。 …除非SSD支持NCQ(本机命令队列),它使SSD能够从队列中选择接下来要执行的操作,以便最大限度地利用其所有芯片。 所以你所看到的可能是NCQ的好处。 (请注意,NCQ也适用于硬盘驱动器)。

由于SSD之间的差异(控制器策略,内部芯片数量,可用空间等),并行化的好处可能会有很大差异。