10

我有这个简单的循环:

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

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

sum = array.Sum();

该代码比 C# 中的原始循环慢很多倍

有人可以告诉我是否可以做更多的事情来提高此循环的性能(无需将其编译为 64 位版本 - 速度快两倍)。

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

编辑:小修正。64 位版本使用双精度而不是整数快两倍

4

7 回答 7

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
于 2013-10-13T16:10:39.587 回答
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("===");
        }

进一步玩弄,事实证明,多个聚合变量什么都不做。不过,展开循环确实有了很大的改进。Unsafe 什么也没做(除非在展开的情况下非常需要它)。展开 2 次与 4 次一样好。

在 Core i7 上运行它。

于 2013-10-13T16:34:06.127 回答
7

首先是关于微基准的一些一般性评论,如下所示:

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

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

  1. 并行化它。这是显而易见的。
  2. 展开循环。这通过仅每 2 次或更多次迭代进行比较来减少指令的数量。
  3. 使用不安全的代码。在这种情况下,这并没有太大的好处,因为主要问题(对数组的范围检查)已被优化掉。
  4. 让代码得到更好的优化。我们可以通过将实际的基准代码放在一个单独的方法中来做到这一点。

这是并行代码:

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 位代码之间没有显着差异。

于 2013-10-13T19:43:08.753 回答
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]; 
于 2013-10-13T21:32:23.723 回答
0

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

优化它。

于 2013-10-20T10:16:43.127 回答
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 / 8,,arr.Length / 16arr.Length / 32我得到了 i7 cpu(4 核 * 2 线程 = 8 线程同时))它们几乎都一样,所以这是你的选择

编辑:我也试过stepsize = arr.length / 100了,在某处@ 250ms,所以有点慢。

于 2013-10-13T17:17:22.970 回答
0

一个经常被忽略的简单且有时重要的 C#for循环优化是将循环计数器变量类型intuint. i++对于具有数百万次迭代的标准(增量)循环,这会导致平均约 12% 的加速。如果您的循环迭代次数少于此值,则可能不会对性能产生太大影响。

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

于 2015-12-18T21:52:58.113 回答