帮助理解C#优化

时间:2011-02-09 05:28:41

标签: c# .net multithreading parallel-processing

我正在玩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();
        }
    }
}

7 个答案:

答案 0 :(得分:1)

结构的大小通常应小于指针的大小(如果性能是主要问题。Microsoft says如果不需要引用类型语义,那么小于16字节的任何东西作为结构都会表现得更好,否则传递它的开销会增加(因为它是通过值传递)并且会比传递指针更多。你的struct包含一个指针和一个int(使它不仅仅是一个指针),所以你会遇到这样的开销。

请参阅this article何时使用结构部分。

答案 1 :(得分:1)

我尝试通过分析器运行它,但我不相信我得到的结果。 (Run1占用的时间比Run2少。)所以那里没有任何具体的答案,但我怀疑有效的[]数组是罪魁祸首:

  1. 这是Run1正在进行的潜在大内存分配而Run2不是。分配大块内存可能非常耗时。

  2. 有可能数组最终远离物理内存中的任何其他工作数据。至少,它足够大到最终在大对象堆中,而看起来大多数其他东西都将最终在堆栈或小对象堆上。这可能意味着Score1函数必须处理比Score2函数更多的缓存未命中。

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

答案 2 :(得分:1)

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

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

在那里你会看到生成的代码是完全不同的,所以在一个很长的列表中,你会为那些额外的几行删掉一些cicles,它会改变最后的时间。

答案 3 :(得分:1)

我刚刚浏览了你的代码,我的第一个想法是行动时间。 在Score1中,您为每次运行执行新的内存分配

valid[i] = new ValidWord 

这反过来让应用程序进程内存找到然后初始化它或创建一个新的内存块,设置值并将新创建的块复制到原始位置(我忘记了,虽然不是重点。

我想说的是,你现在要求应用程序进行14000次内存读/写操作,所有这些操作都需要x个(微秒)。如果正在分配新内存,则需要找到正确大小的内存部分。

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

嗯,这是我的2美分价值,希望它能让你更好地了解要寻找什么。 我家里有一本好书,介绍如何分析你的代码行以及它将使用的处理时间。将看看我是否可以暂停(最近移动)并为您更新名称。

答案 4 :(得分:1)

该计划的目标是什么? Score1和Score2并没有告诉我们算法正在尝试做什么。它似乎喜欢它试图找到任何三个字母的单词,所有大写'U'被删除是一个有效的单词并被添加到列表中?

当每个东西传递完全相同的输入时,在一堆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调用。

Score1和Score2的速度大致相同,因为在StringBuilder.ctor和clr.dll中有额外的时间,Score1会略微变慢。

但是我怀疑你的算法可以在没有所有字符串构建器或分配速度快的情况下被重写。

答案 5 :(得分: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);

答案 6 :(得分:-1)

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

foreach (char c in words[i])
  1. 我认为为当前单词创建一个临时变量会更好。

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

  3. 代码会变成这样:

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

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