我在这里读到了一些答案(对于example),有人说并行性不会提高性能(可能在读取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-并行 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我没有用硬盘测试它(我很高兴有硬盘的人会运行测试。)
答案 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资源非常闲置?我会在接下来尝试两件事:
更新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之间存在差异(控制器策略,内部芯片数量,可用空间等),并行化的好处可能会有很大差异。