C# 我能做些什么使这个循环运行得更快?
我有一个简单的循环:C# 我能做些什么使这个循环运行得更快?,c#,performance,for-loop,C#,Performance,For Loop,我有一个简单的循环: int[] array = new int[100000000]; int sum = 0; for (int i = 0; i < array.Length; i++) sum += array[i]; 该代码比C中的原始循环慢很多倍# 有没有人能告诉我,我还可以做些什么来提高这个循环的性能(而不必将它编译成64位版本——速度是原来的两倍) 所有测试均在32位发布版本上进行,并且在没有调试器的情况下运行 编辑: 小修正。64位版本使用Double,而不是i
int[] array = new int[100000000];
int sum = 0;
for (int i = 0; i < array.Length; i++)
sum += array[i];
该代码比C中的原始循环慢很多倍#
有没有人能告诉我,我还可以做些什么来提高这个循环的性能(而不必将它编译成64位版本——速度是原来的两倍)
所有测试均在32位发布版本上进行,并且在没有调试器的情况下运行
编辑:
小修正。64位版本使用Double,而不是ints,速度快两倍。将循环展开2-8次。衡量哪一个是最好的。NET JIT的优化效果很差,因此您必须完成它的一些工作
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);
您可能还必须添加不安全的,因为JIT现在无法优化数组边界外检查
您还可以尝试聚合为多个总和变量:
int sum1 = 0, sum2 = 0;
for (int i = 0; i < array.Length; i+=2) {
sum1 += array[i+0];
sum2 += array[i+1];
}
进一步研究发现,多个聚合变量没有任何作用。不过,展开循环做了一个重大改进。不安全没有做任何事情(除了在展开的情况下,这是非常必要的)。展开2次相当于4次
在Core i7上运行此功能。我在@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的某个位置,所以有点慢。首先是关于微基准测试的一些一般性评论,如下所示:
sum = array.Sum();
- 这里的时间非常短,因此JIT时间可能非常重要。这一点很重要,因为并行
ForEach
循环包含一个匿名委托,该委托仅在第一次调用时进行JIT,因此JIT时间包含在第一次运行基准的计时中
- 代码的上下文也很重要。抖动可以更好地优化小功能。在其自身的功能中隔离基准代码可能会对性能产生重大影响
有四种加速代码的基本技术(如果我们保持代码为纯CLR):
将其并行化。这是显而易见的
展开循环。这减少了指令的数量,只需每2次或更多次迭代进行一次比较
使用不安全代码。在这种情况下,这不是什么好处,因为主要问题(阵列上的范围检查)已经优化了
允许更好地优化代码。我们可以通过将实际的基准代码放在一个单独的方法中来实现这一点
以下是并行代码:
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; } });
32位和64位代码之间没有显著差异。使用立即操作数将在一定程度上提高性能
int[] array = new int[100000000];
int sum = 0;
for (int i = 0; i < array.Length; i++)
sum += array[i];
不安全和并行代码也应该提高性能。查看本文了解更多信息
循环优化的一个简单且有时意义重大的C#,经常被忽略,就是将循环计数器变量类型从int
切换到uint
。这将导致标准i++
(增量)循环在数百万次迭代中平均加速约12%。如果循环的迭代次数少于此值,那么性能可能不会有太大的变化
请注意,可以通过uint
对数组进行索引,而无需强制转换为int
,因此在循环内进行索引时不会失去任何好处。不使用此技术的唯一常见原因是,如果需要负循环计数器值,或者如果循环中的其他函数调用等需要将循环计数器变量转换为int
。当你需要投掷的时候,它可能不值得。为什么64比特的速度快2倍?C++,完全编译。C#是预编译的,然后由clr解释。@Silvermind:我认为“unchecked”对double没有影响,只对整数有影响。我还认为编译器的默认选项是“unchecked”,我的时钟是310毫秒对420毫秒。不错,考虑到我的C++编译器自动并行化循环。您可能犯了传统的基准测试错误,例如测量jitting时间、运行调试生成或连接调试器以禁用优化器。您确定这是应用程序中的瓶颈吗?您能发布结果吗?谢谢。这种简单的循环不能像非托管代码那样快速运行,这一点并不令人遗憾。特别是在这一点上,没有涉及GC,也没有进行范围检查;linq和:751ms;对于固定回路:76ms;每次总和:77ms;linq并行和:183ms。这是在使用i7 920时启用了超读功能。对于OP的100000000个元素,它甚至不需要旋转每个可用的核心;将元素的数量增加一倍就可以了。此外,我还考虑了watch.elapsedmillesons
,因为这会对报告的计时产生影响。在debug/release中运行此测试会产生很大的影响。对于循环:430/49毫秒,Ling sum:724/714毫秒,对于循环fixed:393/48毫秒,Foreach sum:492/49毫秒,Ling parallel:168/148毫秒。记住在测试时在发布模式下运行检查代码。IMHO,这并没有回答比较速度的基本问题,但它确实加快了C#。在公平的比较中,我们将重写C++版本并行,然后发现C++代码仍然是C++中的2倍慢的当前等价循环,你提到.NET JIT优化得很差,我感兴趣并实现了A比较。与我笔记本电脑上的C#解决方案相比,它的运行速度要快得多。Java真的优化了吗?我在一个循环中运行了所有测试100次。与我最快的JavaRecursiveTask
实现(最小:21ms)相比,并行linq解决方案最快,最小:158ms。这个
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; } });
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
int[] array = new int[100000000];
int sum = 0;
for (int i = 0; i < array.Length; i++)
sum += array[i];
int[] array = new int[100000000];
int sum = 0;
int arrayLength=array.length;
for (int i = arrayLength-1; i >0; i--)
sum += array[i];