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并不总是最好的.
这是一个非常有趣的话题!对不起,我无法解释技术细节,但有一些问题需要提出.它有点长,所以我无法将它们纳入评论.请原谅我把它作为"答案"发布.
我认为您需要考虑大文件和小文件,测试必须运行几次并获得平均时间以确保结果可验证.一般指导原则是在进化计算中建议将其运行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资源非常闲置?接下来我会尝试两件事:
更新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的讨论的原因.
此行为的原因称为文件缓存,它是一种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密集型的,但它不可忽略不计.由于文件已经在系统缓存中,因此并行方法(没有缓存修改)准确显示了这些开销操作的时间.
此行为也可以转移到写入操作.
首先,测试需要排除任何CPU/RAM操作(GetHashCode),因为在执行下一个磁盘操作之前串行代码可能正在等待CPU.
在内部,SSD总是试图在其不同的内部芯片之间并行化操作.它的能力取决于模型,它有多少(TRIMmed)自由空间等.直到前一段时间,这应该在parallell和serial中表现相同,因为OS和SSD之间的队列无论如何都是串行的. ...除非SSD支持NCQ(本机命令队列),这使得SSD能够从队列中选择接下来要执行的操作,以便最大限度地利用其所有芯片.所以你所看到的可能是NCQ的好处.(请注意,NCQ也适用于硬盘驱动器).
由于SSD之间的差异(控制器策略,内部芯片数量,可用空间等),并行化的好处可能会有很大差异.
| 归档时间: |
|
| 查看次数: |
2916 次 |
| 最近记录: |