循环优化

3

我正在编写一些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中的值将永远变化为无法配对的double值。

请参见“Stack Overflow不允许在标题中使用标签”。 - John Saunders
4
ClassA.Multiplier和ClassB.Value是什么类型?它们的getter方法是如何定义的? - Karl-Johan Sjögren
2
在多个核心上运行此代码将不如多次运行此项目。为什么呢? - Didaxis
1
我曾经读到一个有趣的答案。你可以将数组分成4个不同的部分,并运行4个并行进程来求和,然后将4个结果相加。 - Sebas
1
@S_BatMan,这个问题有几点让我怀疑这段代码是否真的是问题所在。例如,它被声明为“void”,实际上并没有执行任何操作。你对@ErOx的回复是基于假设而不是数据。你没有告诉我们ClassA是什么,或者它填充了什么。所展示的代码可能成为瓶颈,但由于它不能编译或执行任何操作,显然你没有给我们展示所有的东西。 - Dour High Arch
显示剩余8条评论
11个回答

11
你应该删除这个:
int length = CAArray.Length;

并将循环替换为以下内容:

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

像您原始代码中存储长度的方式实际上会减慢C#代码的运行(这与直觉相反)。这是因为如果您在for循环中直接使用Array.Length,JIT编译器将在每次迭代时跳过执行数组边界检查。

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

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

尽管您可以潜在地在没有LINQ的情况下获得更快的速度(但这时需要担心管理多个线程的低级细节)。


我很失望,因为被接受的答案(并且得票最高)没有提到这一点。+1 - ChimeraObscura

7

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

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

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

另一件事情是,如果可能的话,将ClassB、ClassB.Value和Multiplier属性作为字段。

最后 - 记得在解决方案属性中勾选“优化代码”,让编译器优化你的代码。


6

尝试:

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

2
在迭代循环中只使用一个索引访问。 - Tigran
@TimS.:这个问题涉及到微小优化。在这里改变的空间不多。 - Tigran
进一步的微观优化是将“var a”的声明移到循环之外。然后将“a.ClassB.Value”和“a.Multiplier”更改为公共字段而不是属性。 - bluevector
@YoryeNathan 我不知道那个。当然,我只是出于习惯这样做,因为我变老了。 - bluevector
2
@jonnyGold 声明变量只有在你真正需要它们的时候才是一个好习惯 - 这可以提高可读性,而且似乎不会对性能产生影响(几乎从来没有)。 - SimpleVar
显示剩余5条评论

3

另一种微小的优化方式是使用field而非property,尤其对于大型数据集性能的影响可能会更加明显。

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

即使使用属性是微软建议的指南,但众所周知,属性引入了非常小的开销(但在处理大数据时可能会很重要)。
希望这可以帮到您。

2
+1 作为尝试的想法,但我期望JIT内联简单属性。因此,请尝试并仔细分析性能,如果看到显着的好处,则切换。 - Alexei Levenkov

1

我不知道你对 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
@S_BatMan:在你的方法中,乘法是在读取“MultipliedValue”时还是在写入值时进行的? - Austin Salonen
关于CAArray [i] .mutiplier的调整,将其放在CAArray [i] .classb内部执行会使该变量距离函数多出一步。 - S_BatMan

1

如果您在乘法器和 ClassB.Value 方面存在大量重复,您可能希望找到所有不同的配对,将每个配对乘以一次,然后乘以此配对出现次数。

此外,我建议使用 AsParallel() 并利用所有核心。


我怀疑区分元素所需的时间比仅仅乘以每个元素要少。 - SimpleVar
也许分组已经在上游某个地方可用并且可以被重复使用。不知道数据分布情况很难说。 - Jakub Konecki
虽然这是一个很好的建议,但我已经声明希望它保持单核心,而且这些值将会永远变化,因此这是不可能的。 - S_BatMan

0
由于数组具有大量元素,像这样的方法比其他迭代循环的方法更快。
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 中的一个奇怪的 bug,如果将 for 循环放在 try catch 中,它会运行得更快,因为在该情况下,Jitter 更倾向于使用寄存器而不是 CPU 栈来存储本地变量。

0

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

额外的工作可能很少或很多,但肯定不会比零少,与预增量相比,预增量只需执行增量,然后返回刚刚更改的值--无需复制//保存//等。


我认为抖动会处理这个问题。 - Jakub Konecki

0
  1. 并行化。
  2. 尝试展开循环。(编译器可能会自动执行此操作。)

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接