2011-02-09 149 views
12

我在玩C#并希望加快一个程序。我做了改变,并能够这样做。但是,我需要帮助理解为什么变化更快。帮助理解C#优化

我试图减少代码,以更容易理解的问题。 Score1和Report1是较慢的方法。 Score2和Report2是更快的方法。第一种方法首先在并行结构中存储一个字符串和一个int。接下来,在一个串行循环中,它循环遍历这些结构的数组并将它们的数据写入缓冲区。第二种方法首先将数据并行写入字符串缓冲区。接下来,在串行循环中,它将字符串数据写入缓冲区。下面是一些样品运行时间:

运行1总平均时间= 0.492087秒 运行2总平均时间= 0.273619秒

当我随着早期非水货版的这个工作,时间几乎一样。为什么与平行版本有所不同?

即使我减少Report1中的循环以将单行输出写入缓冲区,它仍然较慢(总时间约为.42秒)。

这里是简化代码:

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Diagnostics; 
using System.Threading.Tasks; 
using System.IO; 

namespace OptimizationQuestion 
{ 
    class Program 
    { 
     struct ValidWord 
     { 
      public string word; 
      public int score; 
     } 
     ValidWord[] valid; 
     StringBuilder output; 
     int total; 

     public void Score1(string[] words) 
     { 
      valid = new ValidWord[words.Length]; 

      for (int i = 0; i < words.Length; i++) 
      { 
       StringBuilder builder = new StringBuilder(); 

       foreach (char c in words[i]) 
       { 
        if (c != 'U') 
         builder.Append(c); 
       } 
       if (words[i].Length == 3) 
       { 
        valid[i] = new ValidWord 
        { word = builder.ToString(), score = words[i].Length }; 
       } 
      } 
     } 
     public void Report1(StringBuilder outputBuffer) 
     { 
      int total = 0; 
      foreach (ValidWord wordInfo in valid) 
      { 
       if (wordInfo.score > 0) 
       { 
        outputBuffer.AppendLine(String.Format("{0} {1}", wordInfo.word.ToString(), wordInfo.score)); 
        total += wordInfo.score; 
       } 
      } 
      outputBuffer.AppendLine(string.Format("Total = {0}", total)); 
     } 

     public void Score2(string[] words) 
     { 
      output = new StringBuilder(); 
      total = 0;   
      for (int i = 0; i < words.Length; i++) 
      { 
       StringBuilder builder = new StringBuilder(); 

       foreach (char c in words[i]) 
       { 
        if (c != 'U') 
         builder.Append(c); 
       } 
       if (words[i].Length == 3) 
       { 
        output.AppendLine(String.Format("{0} {1}", builder.ToString(), words[i].Length)); 
        total += words[i].Length; 
       } 
      } 
     } 
     public void Report2(StringBuilder outputBuffer) 
     { 
      outputBuffer.Append(output.ToString()); 
      outputBuffer.AppendLine(string.Format("Total = {0}", total)); 
     } 
     static void Main(string[] args) 
     { 
      Program[] program = new Program[100]; 
      for (int i = 0; i < program.Length; i++) 
       program[i] = new Program(); 

      string[] words = File.ReadAllLines("words.txt"); 

      Stopwatch stopwatch = new Stopwatch(); 
      const int TIMING_REPETITIONS = 20; 
      double averageTime1 = 0.0; 
      StringBuilder output = new StringBuilder(); 
      for (int i = 0; i < TIMING_REPETITIONS; ++i) 
      { 
       stopwatch.Reset(); 
       stopwatch.Start(); 
       output.Clear(); 
       Parallel.ForEach<Program>(program, p => 
        { 
         p.Score1(words); 
        }); 
       for (int k = 0; k < program.Length; k++) 
        program[k].Report1(output); 
       stopwatch.Stop(); 
       averageTime1 += stopwatch.Elapsed.TotalSeconds; 
       GC.Collect(); 
      } 
      averageTime1 /= (double)TIMING_REPETITIONS; 
      Console.WriteLine(string.Format("Run 1 Total Average Time = {0:0.000000} sec", averageTime1)); 
      double averageTime2 = 0.0; 
      for (int i = 0; i < TIMING_REPETITIONS; ++i) 
      { 
       stopwatch.Reset(); 
       stopwatch.Start(); 
       output.Clear(); 
       Parallel.ForEach<Program>(program, p => 
        { 
         p.Score2(words); 
        }); 
       for (int k = 0; k < program.Length; k++) 
        program[k].Report2(output); 
       stopwatch.Stop(); 
       averageTime2 += stopwatch.Elapsed.TotalSeconds; 
       GC.Collect(); 
      } 
      averageTime2 /= (double)TIMING_REPETITIONS; 
      Console.WriteLine(string.Format("Run 2 Total Average Time = {0:0.000000} sec", averageTime2)); 
      Console.ReadLine(); 
     } 
    } 
} 
+5

为什么你试图排名这样不同的代码为报表和报告2? Report1包含一个循环,Report2不包含。也许在非并行版本中,C#编译器展开了循环或其他魔法? – Earlz 2011-02-09 05:32:27

+0

将Report1循环减少为一次迭代有一点帮助(.42秒),但发布后,我认为它是Score1中的数组分配。 – jlim 2011-02-09 05:35:52

+1

注意:单词列表大约是14,000行字符串。因此,每次调用score1分配14,000个结构。 – jlim 2011-02-09 06:02:11

回答

1

首先,您将重复运行并行化。这会提高你的基准时间,但可能不会帮助你真正的生产运行时间。为了准确地测量实际运行一个单词列表需要多长时间,您需要一次只准备一个单词列表。否则,处理所有列表的单个线程在某种程度上相互竞争,因为系统资源和每个列表的时间受到影响,即使总共完成所有列表的时间更快。

要加快处理一个单词列表的时间,您希望并行处理列表中的单个单词,一次只能处理一个列表。为了获得足够的定义/尺寸以便进行良好的测量,可以将列表设置得很长,或者连续多次处理列表。

在你的情况下,这有点棘手,因为最终产品所需的stringbuilder没有记录为线程安全的。尽管如此,这并没有那么糟糕。这里的呼吁并行的foreach单个单词列表的例子:

var locker = new Object(); //I'd actually make this static, but it should end up as a closure and so still work 
var OutputBuffer = new StringBuilder(); // you can improve things futher if you can make a good estimate for the final size and force it allocate all the memory it will need up front 
int score = 0; 
Parallel.ForEach(words, w => 
{ 
    // We want to push as much of the work to the individual threads as possible. 
    // If run in 1 thread, a stringbuilder per word would be bad. 
    // Run in parallel, it allows us to do a little more of the work outside of locked code. 
    var buf = new StringBuilder(w.Length + 5); 
    string word = buf.Append(w.Where(c=>c!='U').Concat(' ').ToArray()).Append(w.Length).ToString(); 

    lock(locker) 
    { 
     OutputBuffer.Append(word); 
     score += w.Length; 
    } 
}); 
OutputBuffer.Append("Total = ").Append(score); 

只需调用,在正常的连续20次处理的循环。再说一遍,它可能会让基准测试速度稍慢一些,但由于基准测试中存在缺陷,我认为它会以更快的速度执行现实世界。还请注意,我在回复窗口中输入了这个权限—我从来没有事件试图编译它,所以它不可能完美无缺。

固定的基准,以更准确地反映代码的并行会影响您的真实世界的处理时间后,下一步是做一些profiling,看看你的程序实际上是花钱的时候。这就是你如何知道需要改进的地方。

出于好奇,我也想知道这个版本如何执行:

var agg = new {score = 0, OutputBuffer = new StringBuilder()}; 
agg = words.Where(w => w.Length == 3) 
    .Select(w => new string(w.Where(c => c!='U').ToArray()) 
    .Aggregate(agg, (a, w) => {a.OutputBuffer.AppendFormat("{0} {1}\n", w, w.Length); score += w.Length;}); 
agg.OutputBuffer.Append("Total = ").Append(score); 
1

一个结构的尺寸通常应当小于一个指针(如果性能是首要的问题Microsoft says即任何小于16个字节执行作为一个更好的结构如果不需要引用类型语义),否则,传递它的开销会增加(因为它是按值传递的),并且会比仅仅传递指针所需的开销更大。你的结构体包含一个指针和一个int(使它不仅仅是一个指针),所以你会因为这个而遇到开销。

查看何时使用结构部分的this article

+0

*结构的大小通常应小于指针的大小(Microsoft建议不要超过16个字节)* - C#中的引用远不及16个字节。很难做出一个有用的结构类型,它小于参考需要的32/64(+一些内部管理开销)位。 – 2011-02-09 07:40:56

-1

只是一个想法:我没有做任何测量,比如这一行:

foreach (char c in words[i]) 
  1. ,我认为这将是更好的创建当前字一个临时变量。

  2. 此外,字符串的迭代器可能会更慢。

的代码会变得这样的事情:

var currentWord = words[i]; 
for (int j = 0; j < currentWord.Length; j++){ 
    char c = currentWord[i]; 
    // ... 
} 

新的也可能是一个性能问题,因为有人已经精确定位。就像我在我的评论中所说的那样,添加更多分析数据将有助于确切指出发生了什么。或者看看生成的代码。

1

我试着通过探查器运行它,但我不相信我得到的结果。 (RUN1花费的时间比RUN2的时间少了。)所以目前还没有任何具体的答案有,但我怀疑是有效的[]数组是罪魁祸首:

  1. 这是一个潜在的大内存分配是RUN1在做和Run2不是。分配大块内存可能非常耗时。

  2. 这可能是阵列结束远离物理内存中的任何其他工作数据。至少,它足够大,最终可以放入大型对象堆中,而它看起来像其他所有东西最终都会堆放在堆栈或小型对象堆上。这可能意味着Score1函数不得不处理比Score2函数更多的缓存未命中。

在串行代码中,这可能是一个小得多的问题,在任何给定的时间只发生一次。但是,当它同时发生在很多线程中时,问题可能会复杂化,以至于最初导致额外缓存未命中的原因现在会导致页面错误。

1

所以有一个关于codeproject的帖子可以帮助解答这个问题。

http://www.codeproject.com/KB/cs/foreach.aspx

在那里,你将看到生成的代码slitely不同,所以在很长的列表,你将失去一些圈子中那些额外的几行字,它会改变最终的时间。

1

嗯,我刚刚浏览了您的代码,我的第一个想法是行动时间。 在你Score1你每次运行

valid[i] = new ValidWord 

这反过来又进行新的内存分配让应用程序进程的内存发现然后要么初始化它,或者设置的值,并复制在新创建的一个新的内存块创建的块到原来的位置(我忘了哪个,但不是点)。

我试图做的一点是,您现在要求应用程序进行14000次内存读取/写入操作,所有这些操作都需要x个(微秒)的时间。如果正在分配新内存,则需要找到正确大小的内存段。

代码性能分析是一个相当广泛的话题,我猜想只有嵌入式程序员每天真正使用它。请记住,您所做的每项陈述都有与其相关的操作。例如,读取Vector<bool>Vector<int>例如,bool将具有较慢的读取时间,因为它需要将存储器分成几个位然后返回一个值,其中int可以检索更大的存储块。

那么,这是我的2美分价值,希望它能给你一个更好的想法寻找什么。 我在家里有一本很好的书,介绍如何分析你的代码行以及它将使用的处理时间。会看看我是否可以保留它(最近移动)并更新名称。

1

该计划的目标是什么? Score1和Score2并没有告诉我们关于算法试图做什么的任何事情。它看起来很喜欢它试图找到任何单词是三个字母,所有大写'U's删除是一个有效的单词,并添加到列表中?

当每个事物传递完全相同的输入时,在一堆Program实例上调用Parallel.Foreach有什么意义?并且总是为每个单词创建一个StringBuilder不是一个好方法。您希望最小化性能关键区域中的任何新呼叫,以减少GC必须进入的次数。

我跑你的程序上的文本文件:http://introcs.cs.princeton.edu/data/words.txt

  • 运行1总平均时间= 0.160149秒
  • 运行2总平均时间= 0.056846秒

的VS下运行它2010采样分析器显示Report1比Report2慢大约78倍,并且考虑了大部分差异。主要是由于所有的string.Format和Append调用。

由于在StringBuilder.ctor和clr.dll中有额外的时间,Score1和Score2在速度上大致相同,Score1的速度稍慢。

但我怀疑你的算法可以被重写,而不需要所有的字符串构建器或分配都快一个数量级。