.NET并行库测试实例
并行库存应用场景:
并行计算首要目的是提高CPU的计算能力,简单说程序应该是以CPU密集型运算为主的,如果你的程序
是IO(磁盘和网络)密集型运算,并行计算并不能对你的程序有多大的提高。有时反而会有影响。我们
还是以事实来说话:
namespace ParallelTest
{
static class Program
{
static void Compute(int i)
{
Thread.Sleep(20);
}
[STAThread]
static void Main()
{
DateTime start = DateTime.Now;
for(int i=0;i<100;i++){
Compute(i);
}
TimeSpan ts = DateTime.Now - start;
System.Console.WriteLine("TotalMilliseconds is : {0}",ts.TotalMilliseconds);
}
}
}
我们用Thread.Sleep(20);来模拟一个CPU密集型运算,它只和CPU调度有关,不存在IO运算。这个传统的程序
在我的Pentium D 300的机器上跑的时间是3125ms,理论上它应该是2000,但那上方法内运行的代码所占用的时间,
方法帧的生成,CPU的调度都是额外的时间开销,所以这个程序跑了3125ms.这是我运行20次得到的相同的时间
(当然也就是平均时间了)。
现在我们用并行代码来运行这个程序:
namespace ParallelTest
{
static class Program
{
static void Compute(int i)
{
Thread.Sleep(20);
}
[STAThread]
static void Main()
{
DateTime start = DateTime.Now;
Parallel.For(0, 100, delegate(int i) { Compute(i); });
TimeSpan ts = DateTime.Now - start;
System.Console.WriteLine("TotalMilliseconds is : {0}",ts.TotalMilliseconds);
}
}
}
这些我运行20次,它们的时间大多数时候是相同的,有十多次是1093.75,同时还有1109.375,1140.625,
1081.175之类的时间,总之它们的平均时间比3125少了几乎是两倍,也就是两个CPU运行方法中代码的时间应该
为1000ms,而方法帧生成,CPU调度只占用实际代执行的1/10左右。
更强的是当我把Sleep(20)改成Sleep(50)时,我们知道两个CPU执行100次应该至少2500ms,但实际上这个平均值是
2063.75ms,说明并代码并不只是两个CPU的两个线程在运行,一定还利用超线程/纤程等技术。而非并行代码运行
的时间是6250ms,没有任何让人惊喜的地方。
下面我们再来来看一下本地IO的例子。
我把我的一个目录从E分区复制到D分区,目录是一个图片库存,两级子目录,其中都是平时用到的图片和一些不可
告人(别揭发我啊)的图片按类型分类的。
我们先用传统的编程方式来实现:
namespace ParallelTest
{
static class Program
{
static void CopyDir(DirectoryInfo s, DirectoryInfo d)
{
if (!d.Exists)
d.Create();
foreach (DirectoryInfo sd in s.GetDirectories()) {
CopyDir(sd, new DirectoryInfo(Path.Combine(d.FullName,sd.Name)));
}
foreach (FileInfo f in s.GetFiles()) {
f.CopyTo(new FileInfo(Path.Combine(d.FullName,f.Name)).FullName);
}
}
[STAThread]
static void Main()
{
DateTime start = DateTime.Now;
CopyDir(new DirectoryInfo("E://BigTools//lspic"),new DirectoryInfo("d://"));
TimeSpan ts = DateTime.Now - start;
System.Console.WriteLine("times is : {0}",ts.TotalMilliseconds);
}
}
}
这段代码打印的时间是 75187.5ms.
改用并行代码:
namespace ParallelTest
{
static class Program
{
private static void CopyDir(DirectoryInfo s, DirectoryInfo d)
{
if (!d.Exists)
d.Create();
Parallel.Invoke(
() =>
{
Parallel.ForEach(s.GetFiles(), f =>
{
var t = new FileInfo(Path.Combine(d.FullName, f.Name));
f.CopyTo(t.FullName);
});
},
() =>
{
Parallel.ForEach(s.GetDirectories(), subs =>
{
var subd = new DirectoryInfo(Path.Combine(d.FullName, subs.Name));
CopyDir(subs, subd);
});
});
}
[STAThread]
static void Main()
{
DateTime start = DateTime.Now;
CopyDir(new DirectoryInfo("E://BigTools//lspic"),new DirectoryInfo("d://"));
TimeSpan ts = DateTime.Now - start;
System.Console.WriteLine("times is : {0}",ts.TotalMilliseconds);
}
}
}
结果时间只用了33187.5ms,我们看这段程序的两个分支都在并行执行,首先把当前目录中文件和子目录的处理
作为两个匿名方法分配给Parallel.Invoke()方法来并行处理,然后在其中的循环又分别使用并行代码来执行。
时间节省了一倍多。
但是,这并不是真正的IO密集型运算。因为图片文件都在几十k左右,生成C#的目录对象和文件对象本身花的时间
和真正的IO读写的时间比例没有拉开。也就是IO操作还没到饱和。所以并行代码不仅充分利用了CPU,也大大利用
了IO性能。
但当我在另一个目录中放入6个600M左右(Myeclipse7.2的安装文件改名后复制)的文件时,结果就明显了:
非并行代码:231453.125ms,并行代码:573453.125ms.
令人吃惊的是并行代码不但不能提高程序的性能,反而极大地影响性能。无论人多少CPU在工作,磁盘IO的吞吐量是
有限的。而过多的并行操作反而增加了切换的频度,使IO操作本身增加了大量的OverHead.
当并行代码的程序正在运行的时候,我看了一下目标盘同时生成了四个文件,证明了我上面使用超线程或纤程的
猜想,但我的CPU在系统属性中无法看出支持超线程。
如果你亲手试一下这个例子,6个文件正好说明问题。当运行并行代码时,同时有四个线程(或纤程)在运行,因为
刚开运行时目录盘下立即产生四个文件,然后磁盘不停地嘎嘎嘎嘎响,但时间比四个文件单线程执行要多好久。大约在
450000ms才执行完成,然后余下的两个文件同样在并行代码下COPY,时间不很短。说明并行代码在多并发情况下,对密
集型IO操作不但不能提高性能,还大量浪费环境切换的开销。而实际有多少个并发,目前的并行库还不能控制。
即使只有两个文件,非并行代码和并行代码执行的时间分别为73890.625ms,168343.75ms。并行代码多花了一倍多的时间。
真正的本地IO操作C#代码下运行不可能很快,因为它没有DMA通道的支持,从托管代码到系统调用,每一次COPY一定数量的
字节都必须经过5次内核模式/用户模式的上下文切换和3次读缓冲/应用程序内存/写缓冲的复制。解决密集型IO的方案应该
是IO的并行吞吐能力和ZeorCopy之类的DMA通道才是首选,如java的FileChannel可以直接transferTo到一个输出流,比如
网络IO这样在文件和网络IO之间直接建立DMA通道而不需要反复切换和COPY。
所以,任何技术都有它的合适应用场景,比如在一个CPU的机器上单线程无IO操作运算肯定要比多线程还要快,因为无论如何
同时只有一个线程运行,如果没有IO阻塞,多线程反而增加线程调度的开销。并行编程也同样,主要看我们具体的执行逻辑,
根据具体的情况选择适当的技术。