所以我有这两种方法,假设将1000项长整数数乘以2。 第一种方法:
[MethodImpl(MethodImplOptions.NoOptimization)]
Power(int[] arr)
{
for (int i = 0; i < arr.Length; i++)
{
arr[i] = arr[i] + arr[i];
}
}
第二种方法:
[MethodImpl(MethodImplOptions.NoOptimization)]
PowerNoLoop(int[] arr)
{
int i = 0;
arr[i] = arr[i] + arr[i];
i++;
arr[i] = arr[i] + arr[i];
i++;
arr[i] = arr[i] + arr[i];
i++;
............1000 Times........
arr[i] = arr[i] + arr[i];
}
请注意,我仅将此代码用于性能研究,这就是为什么它看起来如此恶心。
令人惊讶的结果是Power
比PowerNoLoop
快了近50%,尽管我检查了它们的反编译IL
来源和{{1}的内容}}循环与for
中的每一行完全相同。
怎么会这样?
答案 0 :(得分:12)
从我的机器进行样本测量,运行测试10次,PowerNoLoop
是第一次:
00:00:00.0277138 00:00:00.0001553
00:00:00.0000142 00:00:00.0000057
00:00:00.0000106 00:00:00.0000053
00:00:00.0000084 00:00:00.0000053
00:00:00.0000080 00:00:00.0000053
00:00:00.0000075 00:00:00.0000053
00:00:00.0000080 00:00:00.0000057
00:00:00.0000080 00:00:00.0000053
00:00:00.0000080 00:00:00.0000053
00:00:00.0000075 00:00:00.0000053
是的,慢了约50%。值得注意的是第一次通过测试时的抖动开销,显然它会烧掉更多核心,试图获得编译的巨大方法。请记住,当您不禁用优化器时,测量会有很大的不同,因此无环路版本会慢大约800%。
总是寻找解释的第一个地方是生成的机器代码,你可以通过Debug&gt;看到它。 Windows&gt;拆卸。主要的麻烦是PowerNoLoop()
方法的序幕。在x86代码中看起来像这样:
067E0048 push ebp ; setup stack frame
067E0049 mov ebp,esp
067E004B push edi ; preserve registers
067E004C push esi
067E004D sub esp,0FA8h ; stack frame size = 4008 bytes
067E0053 mov esi,ecx
067E0055 lea edi,[ebp-0ACCh] ; temp2 variables
067E005B mov ecx,2B1h ; initialize 2756 bytes
067E0060 xor eax,eax ; set them to 0
067E0062 rep stos dword ptr es:[edi]
请注意非常大的堆栈大小,4008字节。对于只有一个局部变量的方法来说,它应该只需要8个字节。额外的4000个是临时变量,我将它们命名为temp2
。它们被rep stos
指令初始化为0,需要一段时间。我无法解释2756。
在非优化代码中,个人添加是非常沉重的事情。我将为您节省机器代码转储并将其写入等效的C#代码中:
if (i >= arr.Length) goto throwOutOfBoundsException
var temp1 = arr[i];
if (i >= arr.Length) goto throwOutOfBoundsException
var temp2 = temp1 + arr[i];
if (i >= arr.Length) goto throwOutOfBoundsException
arr[i] = temp2
一遍又一遍地重复,总共一千次。 temp2
变量是麻烦制造者,每个单独的陈述都有一个。因此向堆栈帧大小添加4000个字节。如果有人在2756猜测,那么我很乐意在评论中听到它。
在方法开始运行之前必须将它们全部设置为0大致是产生50%减速的原因。可能还有一些指令获取和解码开销,它不能轻易地从测量中分离出来。
值得注意的是,当您删除[MethodImpl]属性并允许优化器完成其工作时,它们不被消除。事实上,该方法根本没有优化,当然因为它不想处理如此大量的代码。
结论你应该绘制的是始终将它留给抖动优化器来为你展开循环。它知道的更好。
答案 1 :(得分:2)
Hans Passant似乎已经触及了主要问题,但却错过了一些观点。
首先正如Mark Jansen所说,代码生成器(在JIT中)有一个特殊情况,可以在简单的for循环中删除对简单数组访问的绑定检查。 [MethodImpl(MethodImplOptions.NoOptimization)]
很可能不会影响这一点。你的展开循环必须检查3000次!
下一个问题是从内存中读取数据(或代码)需要更长的时间,然后运行已经在处理器1级缓存中的指令。从CPU到RAM的带宽也是有限的,因此每当CPU从内存中读取指令时,它就无法读取(或更新)数组。一旦Power
中的循环第一次执行,所有处理器指令都将位于第一级缓存中 - 它们甚至可以以部分解码的形式存储。
更新1000个不同的tempN
变量,会将负载放在CPU缓存甚至RAM上(因为CPU不知道它们不会被再次读取,所以必须将它们保存到RAM中)(不使用{ {1}},JIT可以将MethodImplOptions.NoOptimization
变量合并到一些变量中,然后这些变量将适合寄存器。)
现在大多数CPU可以同时运行许多指令(Superscalar),因此很可能同时执行所有循环检查(1&lt; arr.Length)等。来自阵列的存储/加载。即使是循环结束时的条件GoTo也会被Speculative execution(和/或Out-of-order execution)隐藏。
任何合理的CPU都可以在大约从内存中读取/写入值的时间内运行循环。
如果您在20年前在PC上完成了相同的测试,那么很可能您会获得预期的结果。
答案 2 :(得分:1)
因为c#jit编译器被优化以消除边界检查,如果它可以推断出变量不会超出for循环的范围。
for (int i = 0; i < arr.Length; i++)
的情况被优化器捕获,另一种情况则没有。
这是一篇关于它的博客文章,它很长但值得一读:http://blogs.msdn.com/b/clrcodegeneration/archive/2009/08/13/array-bounds-check-elimination-in-the-clr.aspx
答案 3 :(得分:1)
我在测试中没有看到这些结果。我怀疑你的测试可能因垃圾收集而失真。
我的发布版本测试结果如下(使用Visual Studio 2015,.Net 4.6,Windows 10):
64:
Power() took 00:00:01.5277909
PowerNoLoop() took 00:00:01.4462461
Power() took 00:00:01.5403739
PowerNoLoop() took 00:00:01.4038312
Power() took 00:00:01.5327902
PowerNoLoop() took 00:00:01.4318121
Power() took 00:00:01.5451933
PowerNoLoop() took 00:00:01.4252743
86:
Power() took 00:00:01.1769501
PowerNoLoop() took 00:00:00.9933677
Power() took 00:00:01.1557201
PowerNoLoop() took 00:00:01.0033348
Power() took 00:00:01.1119558
PowerNoLoop() took 00:00:00.9588702
Power() took 00:00:01.1167853
PowerNoLoop() took 00:00:00.9553292
代码:
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace ConsoleApplication1
{
internal class Program
{
private static void Main()
{
Stopwatch sw = new Stopwatch();
int count = 200000;
var test = new int[1000];
for (int trial = 0; trial < 4; ++trial)
{
sw.Restart();
for (int i = 0; i < count; ++i)
Power(test);
Console.WriteLine("Power() took " + sw.Elapsed);
sw.Restart();
for (int i = 0; i < count; ++i)
PowerNoLoop(test);
Console.WriteLine("PowerNoLoop() took " + sw.Elapsed);
}
}
[MethodImpl(MethodImplOptions.NoOptimization)]
public static void Power(int[] arr)
{
for (int i = 0; i < arr.Length; i++)
{
arr[i] = arr[i] + arr[i];
}
}
[MethodImpl(MethodImplOptions.NoOptimization)]
public static void PowerNoLoop(int[] arr)
{
int i = 0;
arr[i] = arr[i] + arr[i];
++i;
<snip> Previous two lines repeated 1000 times.
}
}
}