2011-02-09 112 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的速度稍慢。

但我懷疑你的算法可以被重寫,而不需要所有的字符串構建器或分配都快一個數量級。