【问题标题】:Optimisation of for loopfor循环的优化
【发布时间】:2012-06-26 18:51:09
【问题描述】:

我正在编写一些当前应该尽可能快地运行的 c# 代码,通常以 100% 的速度占用一个内核大约 25 分钟。我需要代码保持单核,因为跨多核运行此代码的好处不如同时运行该项目多次

有问题的代码如下:

public Double UpdateStuff(){

    ClassA[] CAArray = ClassA[*a very large number indeed*];
    Double Value = 0;
    int length = CAArray.Length;

    for (int i= 0; i< length ; i++)
        {
         Value += CAArray[i].ClassB.Value * CAArray[i].Multiplier;
        }  
    return Value;
}

根据分析器,该代码区域负责应用程序负载的 78%,因此似乎是一个很好的优化候选者。

注意,函数已从返回类型 void 更改为返回类型 Double,这是伪代码而非实际代码,以便于阅读。

澄清:.net,c#4.0,visual studio 2010,目标机器:windows server 2008 x64。

编辑:进一步澄清:此上下文中的所有变量都是公共的,而不是属性。 CAArray[i].ClassB.Value 中的值将永远变化,无法成对匹配。

【问题讨论】:

  • ClassA.Multiplier 和 ClassB.Value 有哪些类型?这些属性的 getter 是如何定义的?
  • “跨多核运行这段代码不如多次运行这个项目”
  • 我读过一次有趣的答案。您可以将数组拆分为 4 个不同的数组并运行 4 个并行进程来对部分求和,然后对 4 个结果求和。
  • @S_BatMan,关于这个问题的一些事情让我怀疑这段代码是否真的是问题所在。例如。它实际上并没有做任何事情,因为它被声明为void。您对@ErOx 的回复是基于假设而非数据。你没有告诉我们ClassA 是什么或填充它的东西。显示的代码可能是瓶颈,但由于它不会编译或做任何事情,你显然没有向我们展示一切。

标签: c# .net c#-4.0 optimization


【解决方案1】:

你应该删除这个:

int length = CAArray.Length;

并用这个替换循环:

for (int i= 0; i < CAArray.Length; i++)
{
    Value += CAArray[i].ClassB.Value * CAArray[i].Multiplier;
} 

像原始代码一样存储长度实际上会减慢 C# 代码(我知道这违反直觉)。这是因为如果您在 for 循环中直接有 Array.Length,则抖动将跳过对循环的每次迭代执行数组边界检查。

另外,我强烈建议将此过程并行化。最简单的方法是

CAArray.AsParallel().Sum(i => i.ClassB.Value * i.Multiplier);

尽管您可能可能在没有 LINQ 的情况下获得更快的速度(尽管您随后需要担心管理多个线程的低级细节)。

【讨论】:

  • 我很失望接受的答案(也是投票最多的)没有提到这一点。 +1
【解决方案2】:

一个区别是在 for 循环中使用一个临时变量来保存当前值。

第二个区别,可能更重要,是将 CAArray.Length 而不是 count 放在 for 循环边界。编译器会优化这样的循环来消除边界检查。

for (int i = 0; i < CAArray.Length; i++)
{
    var curr = CAArray[i];
    Value += curr.ClassB.Value * curr.Multiplier;
}

如果可以的话,您可以做的另一件事是将 ClassB、ClassB.Value 和 Multiplier 属性设置为字段。

最后 - 记得检查解决方案属性中的“优化代码”,让编译器优化您的代码。

【讨论】:

    【解决方案3】:

    试试:

    for (int i = 0; i < length; i++)
    {
        var a = CAArray[i];
        Value += a.ClassB.Value * a.Multiplier;
    }  
    

    【讨论】:

    • 迭代循环中只有 一个 索引访问。
    • 数组访问非常快。我怀疑这会带来很大的改善。
    • @TimS.:问题是关于 micro 优化。这里没有太多可以改变的地方。
    • 进一步的微优化是将var a 的声明移出循环。然后将a.ClassB.Valuea.Multiplier 设为公共字段与属性。
    • @jonnyGold 仅在真正需要变量时才声明变量是一个好习惯 - 提高可读性并且显然不会影响性能(几乎从不)。
    【解决方案4】:

    另一个优化,可以顺便影响非常大的集合的性能,是定义field,而不是property

    for (int i= 0; i< length ; i++)
    {
        var a = CAArray[i];
        Value += a.ClassB.value_field * a.multiplier_field;
    }  
    

    即使使用属性是 MS 建议的指导方针,众所周知,属性会引入非常小的(但可能与非常大的数据相关)开销。

    希望这会有所帮助。

    【讨论】:

    • +1 作为尝试的想法,但我希望 JIT 内联简单属性。因此,请尝试仔细分析,如果看到显着的好处,请切换。
    【解决方案5】:

    如果您有很多重复 wrt 乘数和 ClassB.Values,您可能希望找到所有不同的对,将每对相乘一次,然后乘以该对的出现次数。

    另外,我会选择 AsParallel() 并使用所有内核。

    【讨论】:

    • 我怀疑区分元素所花费的时间比将每个元素相乘要少。
    • 也许该分组在上游某处可用并且可以重复使用。在不知道数据分布的情况下很难判断。
    • 虽然这是一个很好的建议,但我已经说过我希望它保持单核,但价值观也将永远改变,因此这是不可能的
    【解决方案6】:

    我不知道你对ClassA 有多少控制权,但在我看来,由于MultiplierClassBClassA 的属性,你应该修改ClassA 以计算出这个属性价值。从理论上讲,您已经将所有这些类实例化并设置了它们各自的属性,因此您可以在ClassB.ValueMultiplier 的设置下轻松计算this.ClassB.Value * this.Multiplier 的所需值。通过这种方式,您可以降低此循环的成本,而是将其移向数据的实例化。这是一个值得的权衡吗?您需要更多地了解应用程序中发生的情况才能做出决定,但这会减少此特定功能的工作量。之后您需要做的就是:

    public void UpdateStuff(){
    
        ClassA[] CAArray = ClassA[*a very large number indeed*];
        Double Value = 0;
        int length = CAArray.Length;
    
        for (int i= 0; i< length ; i++)
        {
            Value += CAArray[i].MultipliedClassBValue;
        }
    return Value;
    }
    

    加上这里的优秀人员可以提出的任何进一步改进。

    【讨论】:

    • 虽然这是我的第一种方法,但由于 b 类中的值和乘数值在运行此代码之前每个刻度都发生变化,因此将计算向上移动并没有加快速度
    • @S_BatMan:在你的方法中,乘法是发生在读取 MultipliedValue 还是写入值时?
    • @Austin 关于调整 CAArray[i].mutiplier 就像在 CAArray[i].classb 中做的那样,使变量远离函数
    【解决方案7】:

    另一个轻微的改进是对索引使用前置增量,因为后置增量必须返回迭代器在递增之前的值;因此,在使用适当的增量更改它之前,需要将先前的值复制到某个地方,因此它可以返回。

    额外的工作可能很少或很多,但它肯定不能小于零,与预增量相比,它可以简单地执行增量然后返回刚刚改变的值 -- 没有复制 //保存 // 等等。

    【讨论】:

    • 我认为 jitter 可以解决这个问题。
    【解决方案8】:
    1. 并行化它。
    2. 尝试展开循环。 (编译器可能会自行执行此操作。)

    【讨论】:

      【解决方案9】:

      还有一点需要注意 - 如果您经常分配非常大的数组(86K+ 数据)并且每次大小都不同,因为这些对象是在 LOH 上分配的,因此您可能会过度强调 GC。

      【讨论】:

        【解决方案10】:

        由于数组有大量元素,这样的方法将比其他迭代循环的方法更快。

        try
        {
            for (int i= 0; ; i++)
            {
                var a = CAArray[i];
                Value += a.ClassB.value_field * a.multiplier_field;
            }
        }
        catch (IndexOutOfRangeException)
        { }
        

        虽然不可否认,它看起来相当丑陋,而且绝对不是一种“纯粹”的编程方式。 但同时使用公共字段而不是属性也不是纯粹的。

        除了移除退出条件的好处之外,CLR 2.0 for X86 中的一个奇怪的错误,如果它被 try catch 包围,会使 for 循环运行得更快,因为在这种情况下,Jitter 更喜欢使用寄存器而不是 CPU 堆栈来存储当地人。

        【讨论】:

          【解决方案11】:

          首先,它是一个 void,所以它不应该返回任何东西(或者它应该返回一个 Double)。其次,C# 通常不使用埃及大括号——但这并不重要。

          然后你可以尝试使用 Linq 和 lambdas,我认为它可能会更快 - 至少更干净!

          public void UpdateStuff()
          {
              ClassA[] CAArray = new ClassA[large_number];
              Double Value = CAArray.Select(x => x.ClassB.Value * x.Multiplier).Sum();
          }
          

          【讨论】:

          • Linq 往往会减慢速度。它通常更干净,但速度更慢——您不再只是使用int 进行迭代;您现在正在对一个对象进行方法调用,这可能会在另一个对象上进行方法调用......等等等等。
          猜你喜欢
          • 2011-08-30
          • 1970-01-01
          • 2015-04-15
          • 1970-01-01
          • 2020-11-07
          • 2018-12-23
          • 2016-02-23
          • 2019-10-09
          • 1970-01-01
          相关资源
          最近更新 更多