【问题标题】:Help understanding C# optimization帮助理解 C# 优化
【发布时间】:2011-02-09 05:28:41
【问题描述】:

我在玩 C#,想加快程序的速度。我进行了更改并且能够这样做。但是,我需要帮助来理解为什么更改会使其更快。

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

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

当我使用它的早期非并行版本时,时间几乎相同。为什么与并行版本有区别?

即使我减少 Report1 中的循环以将单行输出写入缓冲区,它仍然较慢(总时间约为 0.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();
        }
    }
}

【问题讨论】:

  • 你为什么要对 Report1 和 Report2 这样不同的代码进行排名? Report1 包含一个循环,而 Report2 没有。也许在非并行版本中,C# 编译器展开了循环或其他一些魔法?
  • 将 Report1 循环减少到一次迭代会有所帮助(0.42 秒),但发布后,我认为这是 Score1 中的数组分配。
  • 注意:单词表大约有14000行字符串。所以每次调用 score1 都会分配 14,000 个结构体。
  • 我认为你应该尝试做一些分析。如果没有适当的测量,很难准确地说出为什么它会变慢。分配确实很昂贵,但是从您之前的评论来看,我认为结构的 new[] 将转换为: malloc(sizeof(struct) * size);这会很快。结构不作为单独的对象存储在数组中,而是组合在一起。
  • 所有这些词的分数不是总是“3”吗?

标签: c# .net multithreading parallel-processing


【解决方案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);

只需在正常的顺序处理 for 循环中调用 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);

【讨论】:

    【解决方案2】:

    结构的大小通常应该小于指针的大小(如果性能是主要问题。Microsoft says 如果不需要引用类型语义,任何小于 16 字节的结构都可以更好地执行),否则传递它的开销会增加(因为它是按值传递的)并且会比仅传递指针的开销更大。您的结构包含一个指针和一个 int(使其不仅仅是一个指针),因此您将因此而遇到开销。

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

    【讨论】:

    • 结构的大小通常应该小于指针的大小(Microsoft 建议不要超过 16 个字节) - C# 中的引用远不及 16 个字节。很难创建一个有用的结构类型,它小于引用所需的 32/64(+ 一些管理开销)位。
    • @Ed S - 这就是为什么它只是建议而不是强制执行。您的结构可能更大,但请记住,这确实会带来性能损失(在决定结构或类时,您应该权衡并考虑微软对许多结构的影响)。
    • 好的,但这是一个糟糕的建议。我的意思是真的,请给我看一个有用的结构,它
    • 结构中没有指针,只有一个reference到一个字符串和一个int
    • 尽管如此,你不能只说“如果你的结构比引用大,性能会更差”。这取决于实际情况。例如,结构可以在空间中为您提供更具确定性的性能,但如果您经常传递它们,您会为创建的副本付出代价。您不能将其归结为“如果您的结构大于引用,则性能受到影响”之类的语句。没那么简单,第一句话还是错的。
    【解决方案3】:

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

    1. 这是一个潜在的大内存分配,Run1 正在执行而 Run2 没有。分配大块内存可能很耗时。

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

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

    【讨论】:

    • 我在分析原始程序时也遇到了一些问题。它指向一个不同的例程作为瓶颈。我很欣赏可能出现页面错误的想法。
    【解决方案4】:

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

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

    在那里你会看到生成的代码略有不同,所以在一个长列表中,你会为那些额外的几行松散几圈,它会改变最后的时间。

    【讨论】:

      【解决方案5】:

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

      valid[i] = new ValidWord 
      

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

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

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

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

      【讨论】:

        【解决方案6】:

        该计划的目标是什么? Score1 和 Score2 并没有告诉我们算法试图做什么。看起来它试图找到任何三个字母且全部大写 'U' 被删除的单词是一个有效的单词并被添加到列表中?

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

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

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

        在 VS 2010 Sampling profiler 下运行它显示 Report1 比 Report2 慢大约 78 倍,并且占了大部分差异。主要是因为所有的 string.Format 和 Append 调用。

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

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

        【讨论】:

        • 我删除了与评分例程不同的输入以简化发布的代码。 +1 花时间对其进行分析。我试图在发帖前进行分析,但遇到了困难。探查器将另一个位置显示为瓶颈。
        【解决方案7】:

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

        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,foreach 版本经过编译器专门优化,不会一遍又一遍地计算Length。很多“我认为”而没有足够的“我测试过”会导致回复不佳。
        • 同意。猜测编译将执行或不执行哪些优化完全没有用。
        • 我不知道为什么人们这么看不起mod...如果你做一些搜索,你会发现foreach和for生成的IL Code是不同的。 (abi.exdream.com/Blog/post/2009/04/22/… 甚至 Oakcool 也为此发布了一个有趣的链接)。另外,我只是给你一些想法,以帮助你调查此事。我不会代替你做分析。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-11-25
        • 1970-01-01
        相关资源
        最近更新 更多