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

时间:2017-06-06 07:12:09

标签: c# multithreading parallel-processing

我在这里读到了一些答案(对于example),有人说并行性不会提高性能(可能在读取IO中)。

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

- 阅读测试:

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

enter image description here

让我们尝试用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-并行   192

- 写测试:

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

enter image description here

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我没有用硬盘测试它(我很高兴有硬盘的人会运行测试。)

4 个答案:

答案 0 :(得分:17)

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

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

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

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

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

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

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

答案 1 :(得分:7)

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

我认为您需要考虑大文件和小文件,测试必须运行几次并获得平均时间以确保结果可验证。一般指导原则是在进化计算中将其作为论文运行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<int> list = Enumerable.Range(0, numFiles).ToList();
List<List<byte>> allBytes = new List<List<byte>>(numFiles);

List<string> files;

int numTests = 1;

List<long> wss = new List<long>(numTests);
List<long> wps = new List<long>(numTests);
List<long> rss = new List<long>(numTests);
List<long> rps = new List<long>(numTests);

List<long> wsh = new List<long>(numTests);
List<long> wph = new List<long>(numTests);
List<long> rsh = new List<long>(numTests);
List<long> rph = new List<long>(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<byte>(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<byte>(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<byte>(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<byte>(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时的其他后台活动。
  3. 更新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的讨论的原因。

答案 2 :(得分:6)

此行为的原因称为文件缓存,这是一项Windows功能,可提高文件操作的性能。我们来看看Windows Dev Center

的简短说明
  

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

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

我们可以使用MSDN中记录的FileStream标记创建FILE_FLAG_NO_BUFFERING来避免此行为。让我们看一下使用此标志的新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密集型的,但它不可忽略不计。由于文件已经在系统缓存中,因此并行方法(没有缓存修改)显示了这些开销操作的时间。

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

答案 3 :(得分:5)

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

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

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