浅尝.NET 4并行计算 效率已大幅提升

开发 后端
.NET 4中关于并行计算方面的提升,让人激动。毕竟这是代表未来开发技术发展的一个方向。本文也将为大家介绍这方面的内容。

随着Visual Studio 2010正式版的发布,我们已经可以用上.NET 4的所有功能。那么对于并行计算的尝试,是本文的重点。51CTO向您推荐《F#函数式编程语言》,以便于您全方位了解.NET 4中不同的部分。

我们都知道CPU的性能至关重要,但主频已经越来越难以提升,纵向发展受限的情况下,横向发展成为必然——核心数开始越来越多。然而多核心的利用、并行计算一直是编程中的难题,大的不说,就说代码的编写,程序员大多都有过痛苦的经历:多线程的程序代码量大,编写复杂,容易出错,并且实际运行效率是否理想也较难保证。

为改善这种状况,.NET 4.0中引入了 TPL(任务并行库),关于TPL,MSDN的简介是:

任务并行库 (TPL) 的设计是为了能更简单地编写可自动使用多处理器的托管代码。使用该库,您可以非常方便地用现有序列代码表达潜在并行性,这样序列代码中公开的并行任务将会在所有可用的处理器上同时运行。通常这会大大提高速度。

简而言之,TPL提供了一系列的类库,可以使编写并行运算的代码更简单和方便。

说起来很简单,我们来看点例子:

  1. void ThreadpoolMatrixMult(int size, double[,] m1, double[,] m2,   
  2.      double[,] result)  
  3.  {  
  4.    int N = size;                             
  5. int P = 2 * Environment.ProcessorCount; // assume twice the procs for   
  6.                                        // good work distribution  
  7.    int Chunk = N / P;                  // size of a work chunk  
  8.    AutoResetEvent signal = new AutoResetEvent(false);   
  9.    int counter = P;                      // use a counter to reduce   
  10.                                         // kernel transitions      
  11.   for (int c = 0; c < P; c++) {         // for each chunk  
  12.     ThreadPool.QueueUserWorkItem(delegate(Object o)  
  13.     {  
  14.       int lc = (int)o;  
  15.   for (int i = lc * Chunk;           // iterate through a work chunk  
  16.      i < (lc + 1 == P ? N : (lc + 1) * Chunk); // respect upper   
  17.                                                // bound  
  18.            i++) {  
  19.         // original inner loop body  
  20.         for (int j = 0; j < size; j++) {  
  21.           result[i, j] = 0;  
  22.           for (int k = 0; k < size; k++) {  
  23.             result[i, j] += m1[i, k] * m2[k, j];  
  24.           }  
  25.         }  
  26.       }  
  27.    if (Interlocked.Decrement(ref counter) == 0) { // use efficient   
  28.                                                 // interlocked   
  29.                                                // instructions        
  30.         signal.Set();  // and kernel transition only when done  
  31.       }  
  32.     }, c);   
  33.   }  
  34.   signal.WaitOne();  

很眼熟但同时看着也很心烦的代码吧。在换用TPL后,上面的代码就可以简化为:

  1. void ParMatrixMult(int size, double[,] m1, double[,] m2, double[,] result)  
  2.  {  
  3.    Parallel.For( 0, size, delegate(int i) {  
  4.      for (int j = 0; j < size; j++) {  
  5.       result[i, j] = 0;  
  6.        for (int k = 0; k < size; k++) {  
  7.          result[i, j] += m1[i, k] * m2[k, j];  
  8.        }  
  9.      }  
  10.   });  

舒服多了吧?具体的内容请见MSDN的文章 优化多核计算机的托管代码

装好正式版的VS2010以后,写了段代码来测试下,TPL究竟好不好用。

代码很简单,拿一条字符串和一堆字符串里的每一条分别用LevenshteinDistance算法做字符串相似程度比对。先用传统的顺序执行的代码跑一遍,记录下时间;再换用TPL的并行代码跑一遍,记录下时间。然后比对两次运行的时间差异。

  1. using System;  
  2.   using System.Collections.Generic;  
  3.   using System.Linq;  
  4.   using System.Text;  
  5.   using System.Threading.Tasks;  
  6.   using System.Diagnostics;  
  7.     
  8.   namespace ParallelLevenshteinDistance  
  9.   {  
  10.      class Program  
  11.      {  
  12.          static void Main(string[] args)  
  13.          {  
  14.              Stopwatch sw;  
  15.    
  16.              int length;  
  17.              int count;  
  18.              string[] strlist;  
  19.              int[] steps;  
  20.              string comparestring;  
  21.    
  22.              Console.WriteLine("Input string lenth:");  
  23.              length = int.Parse(Console.ReadLine());  
  24.    
  25.              Console.WriteLine("Input string list count:");  
  26.              count = int.Parse(Console.ReadLine());  
  27.    
  28.              comparestring = GenerateRandomString(length);  
  29.              strlist = new string[count];  
  30.              steps = new int[count];  
  31.    
  32.              // prepare string[] for comparison  
  33.              Parallel.For(0, count, delegate(int i)  
  34.              {  
  35.                  strlist[i] = GenerateRandomString(length);  
  36.              });  
  37.    
  38.              Console.WriteLine("{0}Computing...{0}", Environment.NewLine);  
  39.    
  40.              // sequential comparison  
  41.              sw = Stopwatch.StartNew();  
  42.              for (int i = 0; i < count; i++)  
  43.              {  
  44.                  steps[i] = LevenshteinDistance(comparestring, strlist[i]);  
  45.              }  
  46.              sw.Stop();  
  47.              Console.WriteLine("[Sequential] Elapsed:");  
  48.              Console.WriteLine(sw.Elapsed.ToString());  
  49.    
  50.              // parallel comparison  
  51.              sw = Stopwatch.StartNew();  
  52.              Parallel.For(0, count, delegate(int i)  
  53.              {  
  54.                  steps[i] = LevenshteinDistance(comparestring, strlist[i]);  
  55.              });  
  56.              sw.Stop();  
  57.              Console.WriteLine("[Parallel] Elapsed:");  
  58.              Console.WriteLine(sw.Elapsed.ToString());  
  59.                            
  60.              Console.ReadLine();  
  61.          }  
  62.    
  63.          private static string GenerateRandomString(int length)  
  64.          {  
  65.              Random r = new Random((int)DateTime.Now.Ticks);  
  66.              StringBuilder sb = new StringBuilder(length);  
  67.              for (int i = 0; i < length; i++)  
  68.              {  
  69.                 int c = r.Next(97, 123);  
  70.                  sb.Append(Char.ConvertFromUtf32(c));  
  71.              }  
  72.              return sb.ToString();  
  73.          }  
  74.    
  75.          private static int LevenshteinDistance(string str1, string str2)  
  76.          {  
  77.              int[,] scratchDistanceMatrix = new int[str1.Length + 1, str2.Length + 1];  
  78.              // distance matrix contains one extra row and column for the seed values              
  79.              for (int i = 0; i <= str1.Length; i++) scratchDistanceMatrix[i, 0] = i;  
  80.              for (int j = 0; j <= str2.Length; j++) scratchDistanceMatrix[0, j] = j;  
  81.    
  82.              for (int i = 1; i <= str1.Length; i++)  
  83.              {  
  84.                  int str1Index = i - 1;  
  85.                  for (int j = 1; j <= str2.Length; j++)  
  86.                  {  
  87.                      int str2Index = j - 1;  
  88.                      var cost = (str1[str1Index] == str2[str2Index]) ? 0 : 1;  
  89.    
  90.                      int deletion = (i == 0) ? 1 : scratchDistanceMatrix[i - 1, j] + 1;  
  91.                      int insertion = (j == 0) ? 1 : scratchDistanceMatrix[i, j - 1] + 1;  
  92.                int substitution = (i == 0 || j == 0) ? cost : scratchDistanceMatrix[i - 1, j - 1] + cost;  
  93.    
  94.                      scratchDistanceMatrix[i, j] = Math.Min(Math.Min(deletion, insertion), substitution);  
  95.    
  96.                      // Check for Transposition  
  97.   if (i > 1 && j > 1 && (str1[str1Index] == str2[str2Index - 1]) && (str1[str1Index - 1] == str2[str2Index]))  
  98.                      {  
  99. scratchDistanceMatrix[i, j] = Math.Min(scratchDistanceMatrix[i, j], scratchDistanceMatrix[i - 2, j - 2] + cost);  
  100.                     }  
  101.                 }  
  102.             }  
  103.  
  104.             // Levenshtein distance is the bottom right element  
  105.             return scratchDistanceMatrix[str1.Length, str2.Length];  
  106.         }  
  107.  
  108.     }  
  109. }  

这里只用了最简单的 Parallel.For 方法,代码很简单和随意,但是看看效果还是可以的。

测试机找了不少,喜欢硬件的朋友兴许也能找到你感兴趣的:P

Intel Core i7 920 (4物理核心8逻辑核心,2.66G) + DDR3 1600 @ 7-7-7-24

AMD Athlon II X4 630 (4物理核心,2.8G) + DDR3 1600 @ 8-8-8-24

AMD Athlon II X2 240 (2物理核心,2.8G) + DDR2 667

Intel Core E5300 (2物理核心,2.33G) + DDR2 667

Intel Atom N270 (1物理核心2逻辑核心,1.6G) + DDR2 667

还在VM workstation里跑过,分别VM了XP和WIN7,都跑在上述i7的机器里,各自分配了2个核心。

程序设置每个字符串长1000个字符,共1000条字符串。

每个机器上程序都跑了3遍,取平均成绩,得到下表:

CPU Core Time_Sequential(s) Time_Parallel(s) S/P(%)
Intel Core i7 920 4 Cores, 8 Threads, 2.6G 55.132634 14.645687 376.44%
AMD AthlonII X4 630 4 Cores, 4 Threads, 2.8G 58.10592 17.152494 338.76%
AMD AthlonII X2 240 2 Cores, 2 Threads, 2.8G 66.159735 32.293972 204.87%
Intel E5300 2 Cores, 2 Threads, 2.3G 70.827157 38.50654 183.94%
Intel Atom N270 1 Cores, 2 Threads, 1.6G 208.47852 157.27869 132.55%
VMWin7(2 logic core) 2 Cores, 2 Threads 56.965068 33.069084 172.26%
VMXP(2 logic core) 2 Cores, 2 Threads 59.799399 35.35805 169.13%

可见,在多核心处理器上,并行计算的执行速度都得到了大幅提升,即便是在单核心超线程出2个逻辑核的Atom N270上亦缩短了32.55%的运行时间。在A240上并行计算的效率竟然是顺序计算的204.87% ?!而同样是4核心,i7 920在超线程的帮助下,并行执行效率提升明显高过A630。最后VM里的测试,是否也可以在某种程度上佐证在多核心的调度上,Win7要强过XP呢(纯猜测)?顺带可以看到,同样是i7的硬件环境,单线程宿主OS(Win7)里执行花费55.133秒,VM(Win7)里56.965秒,速度上有约3%的差异。

另外,针对性能较强的i7处理器,加大程序中的2个变量后再做测试,并行执行的效率比得到了进一步的提升。应该是因为创建/管理/销毁多线程的开销被进一步的摊平的缘故。例如在每字符串2000个字符,共2000条字符串的情况下,顺序执行和并行执行的时间分别是07:20.9679066和01:47.7059225,消耗时间比达到了409.42%。

 来几张截图:

 

 

运行结果1

运行结果

从截图中可以发现,这段测试程序在顺序执行的部分,内存占用相对平稳,CPU则大部分核心处在比较空闲的状态。到了并行执行部分,所有核心都如预期般被调动起来,同时内存占用开始出现明显波动。附图是在每字符串2000个字符,共2000条字符串的情况下得到的。

 这里只是非常局部和简单的一个测试,目的是希望能带来一个直观的体验。微软已经提供了一组不错的例子可供参考 Samples for Parallel Programming with the .NET Framework 4  

原文标题:小试 .NET 4.0 之 并行计算

链接:http://www.cnblogs.com/Elvin/archive/2010/04/20/1716258.html

【编辑推荐】

  1. 使用ASP.NET 4的自动启动特性
  2. 详解.NET 4.0并行计算支持历史
  3. 详读.NET 4.0环境配置
  4. 详解.NET 4.0中异常处理方面的新特性
  5. 三方面诠释.NET 4.0的新特性
责任编辑:彭凡 来源: 博客园
相关推荐

2023-10-30 08:57:19

.Net开发并行计算

2009-12-18 09:38:27

.NET 4.0并行计

2012-12-18 15:33:44

递归数据并行计算

2010-03-22 14:45:40

云计算

2021-06-01 05:51:37

云计算并行计算分布式计算

2009-06-28 22:55:00

SAN惠普存储

2011-04-20 17:15:21

并行计算

2011-04-21 09:13:14

并行计算

2012-08-17 09:32:52

Python

2010-03-19 17:23:45

云计算

2014-04-24 10:25:15

2019-04-18 09:15:05

DaskPython计算

2014-01-21 11:16:59

MPI并行计算

2022-11-30 12:41:03

戴尔

2022-03-21 15:24:27

ThingWorxDPMPTC

2010-03-11 15:23:44

Visual Stud

2010-06-10 08:37:04

并行计算

2017-04-24 12:07:44

Spark大数据并行计算

2011-08-29 10:40:02

浪潮英特尔并行计算

2010-06-02 08:53:51

.NET 4并行编程
点赞
收藏

51CTO技术栈公众号