我该怎么做才能使这个循环运行得更快?

时间:2013-10-13 16:00:09

标签: c# performance for-loop

我有这个简单的循环:

int[] array = new int[100000000];
int sum = 0;
for (int i = 0; i < array.Length; i++)
    sum += array[i];

我将其性能与其C ++版本进行了比较。我认为性能应该接近相同,因为它是非常简单的代码,在这种情况下省略了范围检查。但事实证明,C ++版本几乎快了三倍。所以我实现了C#unsafe版本,但性能更差。 Resharper建议将循环转换为linq表达式,如下所示:

sum = array.Sum();

该代码比C#

中的原始循环慢很多倍

有人能告诉我是否还有其他方法可以改善此循环的性能(不将其编译为64位版本 - 速度提高两倍)。

所有在32位Release版本上进行的测试,无需调试器即可运行。

编辑: 小修正。 64位版本的双倍速度快两倍,而不是整数

7 个答案:

答案 0 :(得分:15)

var watch = new Stopwatch();

int[] array = new int[100000000];
for (int i = 0; i < array.Length; i++)
{
    array[i] = 1;
}

watch.Restart();
int sum = 0;
for (int i = 0; i < array.Length; i++)
    sum += array[i];
Console.WriteLine("for loop:" + watch.ElapsedMilliseconds + "ms, result:" + sum);

sum = 0;
watch.Restart();
sum = array.Sum();
Console.WriteLine("linq sum:" + watch.ElapsedMilliseconds + "ms, result:" + sum);

sum = 0;
watch.Restart();
int length = array.Length;
for (int i = 0; i < length; i++)
    sum += array[i];
Console.WriteLine("for loop fixed:" + watch.ElapsedMilliseconds + "ms, result:" + sum);

sum = 0;
watch.Restart();
foreach (int i in array)
{
    sum += i;
}
Console.WriteLine("foreach sum:" + watch.ElapsedMilliseconds + "ms, result:" + sum);

sum = 0;
watch.Restart();
sum = array.AsParallel().Sum();
Console.WriteLine("linq parallel sum:" + watch.ElapsedMilliseconds + "ms, result:" + sum);

Linq Parallel似乎至少在我的机器上禁食了。

修复长度并不重要,但提高了~10%

实际上没有太多可以做的事情,非托管C代码总是会更快。

我的电脑上的结果是:

for loop:      241ms, result:100000000
linq sum:      559ms, result:100000000
for loop fixed:237ms, result:100000000
foreach sum:   295ms, result:100000000
linq parallel: 205ms, result:100000000

答案 1 :(得分:10)

展开循环2-8次。衡量哪一个最好。 .NET JIT优化得很差,所以你必须做一些工作。

您可能还需要添加unsafe,因为JIT现在无法优化数组边界检查。

您还可以尝试聚合成多个和变量:

int sum1 = 0, sum2 = 0;
for (int i = 0; i < array.Length; i+=2) {
    sum1 += array[i+0];
    sum2 += array[i+1];
}

这可能会增加指令级并行性,因为所有add指令现在都是独立的。

i+0自动优化为i


我对它进行了测试,削减了约30%。

重复时,时间稳定。代码:

        Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;

        var watch = new Stopwatch();

        int[] array = new int[500000000];
        for (int i = 0; i < array.Length; i++)
        {
            array[i] = 1;
        }

        //warmup
        {
            watch.Restart();
            int sum = 0;
            for (int i = 0; i < array.Length; i++)
                sum += array[i];
        }

        for (int i2 = 0; i2 < 5; i2++)
        {
            {
                watch.Restart();
                int sum = 0;
                for (int i = 0; i < array.Length; i++)
                    sum += array[i];
                Console.WriteLine("for loop:" + watch.ElapsedMilliseconds + "ms, result:" + sum);
            }

            {
                watch.Restart();
                fixed (int* ptr = array)
                {
                    int sum = 0;
                    var length = array.Length;
                    for (int i = 0; i < length; i++)
                        sum += ptr[i];
                    Console.WriteLine("for loop:" + watch.ElapsedMilliseconds + "ms, result:" + sum);
                }
            }

            {
                watch.Restart();
                fixed (int* ptr = array)
                {
                    int sum1 = 0;
                    int sum2 = 0;
                    int sum3 = 0;
                    int sum4 = 0;
                    var length = array.Length;
                    for (int i = 0; i < length; i += 4)
                    {
                        sum1 += ptr[i + 0];
                        sum2 += ptr[i + 1];
                        sum3 += ptr[i + 2];
                        sum4 += ptr[i + 3];
                    }
                    Console.WriteLine("for loop:" + watch.ElapsedMilliseconds + "ms, result:" + (sum1 + sum2 + sum3 + sum4));
                }
            }

            Console.WriteLine("===");
        }

进一步发挥作用,事实证明多个聚合变量什么都不做。然而,展开循环确实取得了重大进展。不安全没有做任何事情(除非在非常需要的展开情况下)。展开2次与4次一样好。

在Core i7上运行它。

答案 2 :(得分:5)

首先,关于微观基准的一般性评论如下:

  • 这里的时间很短,JIT时间可能很长。这很重要,因为并行ForEach循环包含一个匿名委托,它在第一次调用时只是JITted,因此JIT时间包含在第一次运行基准测试时的时间内。
  • 代码的上下文也很重要。 JITter可以更好地优化小功能。在自己的功能中隔离基准代码可能会对性能产生重大影响。

加速代码有四种基本技术(如果我们保持纯CLR):

  1. 并行化。这很明显。
  2. 展开循环。这通过每2次或更多次迭代进行比较来减少指令数量。
  3. 使用不安全的代码。在这种情况下,这并没有太大的好处,因为主要问题(对阵列的范围检查)已经过优化。
  4. 允许更好地优化代码。我们可以通过将实际的基准代码放在一个单独的方法中来实现。
  5. 以下是并行代码:

    var syncObj = new object();
    Parallel.ForEach(Partitioner.Create(0, array.Length),
        () => 0,
        (src, state, partialSum) => {
            int end = src.Item2;
            for (int i = src.Item1; i < end; i++)
                partialSum += array[i];
            return partialSum;
        },
        partialSum => { lock (syncObj) { s += partialSum; } });
    

    Partitioner类位于System.Collections.Concurrent命名空间中。

    在我的机器上(i7 950,8个逻辑核心),我得到的时间是:

    For loop: 196.786 ms
    For loop (separate method): 72.319 ms
    Unrolled for loop: 196.167 ms
    Unrolled for loop (separate method): 67.961 ms
    Parallel.Foreach (1st time): 48.243 ms
    Parallel.Foreach (2nd time): 26.356 ms
    

    32位和64位代码之间没有显着差异。

答案 3 :(得分:0)

我在@ Ela的回答中添加了以下内容:

            sum = 0;
        watch.Restart();
        var _lock = new object();
        var stepsize = array.Length / 16;
        Parallel.For(0, 16,
            (x, y) =>
            {
                var sumPartial = 0;
                for (var i = x * stepsize; i != (x + 1) * stepsize; ++i)
                    sumPartial += array[i];
                lock (_lock)
                    sum += sumPartial;
            });
        Console.Write("Parallel.For:" +  watch.ElapsedMilliseconds + " ms, result:" + sum);

然后打印结果,以便获得参考值:

for loop:893ms, result:100000000
linq sum:1535ms, result:100000000
for loop fixed:720ms, result:100000000
foreach sum:772ms, result:100000000
Parallel.For:195 ms, result:100000000

正如你所看到的,waaay更快:) 对于Stepsize,我尝试了arr.Length / 8arr.Length / 16arr.Length / 32(我得到了i7 cpu(4核* 2个线程= 8个线程同时))并且它们几乎都是同样,这是你的选择

编辑:我也试过stepsize = arr.length / 100,这是@ 250ms的某个地方,所以有点慢。

答案 4 :(得分:0)

使用立即操作数将在一定程度上改善性能,

int[] array = new int[100000000];
int sum = 0;
for (int i = 0; i < array.Length; i++)
    sum += array[i];

上面的代码需要访问两个内存位置,即int i和array.length;

改为使用,

int[] array = new int[100000000];
int sum = 0;
int arrayLength=array.length;
for (int i = arrayLength-1; i >0; i--)
    sum += array[i]; 

答案 5 :(得分:0)

不安全和并行代码也应该提高性能。查看此文章了解更多信息。

Optimize it.

答案 6 :(得分:0)

经常忽略的一个简单且有时很重要的C#for循环优化是将循环计数器变量类型从int切换到uint。这使得标准i++增量)循环的平均加速率提高了大约12%,并且具有数百万次迭代。如果你的循环迭代次数少于此值,那么它可能不会改变性能。

请注意,数组可以由uint编制索引,而不会转换为int,因此在循环内部编制索引时不会失去任何好处。不使用此技术的唯一常见原因是,如果需要负循环计数器值,或者循环计数器变量需要在循环内转换为int以进行其他函数调用等。一旦你需要施放,它可能不值得。