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

Roy*_*mir 22 c# parallel-processing multithreading

我在这里读了一些答案(例如),其中一些人说并行性不会提高性能(可能在读取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();
Run Code Online (Sandbox Code Playgroud)

结果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();
Run Code Online (Sandbox Code Playgroud)

结果1:

运行WRITE serial 2028

运行WRITE Parallel 368

结果2:

运行WRITE serial 784

运行WRITE Parallel 426

题:

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

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

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

Han*_*ant 18

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

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

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

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

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

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

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

  • 也许是时候得出结论:您拥有一块非常好的 SSD。不知道它的作用是什么,无法获得有关控制器的技术信息。顺便说一句,拥有好的硬件在开发机器上并不总是有益的,而且往往无法在用户机器上很好地重现。 (3认同)

Tid*_* Gu 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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

我的机器是: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()}");
Run Code Online (Sandbox Code Playgroud)

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

另一个问题是我使用新创建的文件进行读取测试.理论上最好不要这样做(甚至重新启动计算机/填写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
Run Code Online (Sandbox Code Playgroud)

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


Fru*_*erg 6

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

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

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

我们可以FileStream通过使用MSDN上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);
    }
}
Run Code Online (Sandbox Code Playgroud)

结果:读串行速度要快得多.在我的情况下甚至快两倍.

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

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


Jan*_*nar 5

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

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

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