我有这个简单的循环:
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位版本的双倍速度快两倍,而不是整数
答案 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)
首先,关于微观基准的一般性评论如下:
ForEach
循环包含一个匿名委托,它在第一次调用时只是JITted,因此JIT时间包含在第一次运行基准测试时的时间内。加速代码有四种基本技术(如果我们保持纯CLR):
以下是并行代码:
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 / 8
,arr.Length / 16
,arr.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)
不安全和并行代码也应该提高性能。查看此文章了解更多信息。
答案 6 :(得分:0)
经常忽略的一个简单且有时很重要的C#for
循环优化是将循环计数器变量类型从int
切换到uint
。这使得标准i++
(增量)循环的平均加速率提高了大约12%,并且具有数百万次迭代。如果你的循环迭代次数少于此值,那么它可能不会改变性能。
请注意,数组可以由uint
编制索引,而不会转换为int
,因此在循环内部编制索引时不会失去任何好处。不使用此技术的唯一常见原因是,如果需要负循环计数器值,或者循环计数器变量需要在循环内转换为int
以进行其他函数调用等。一旦你需要施放,它可能不值得。