我的硬件是罪魁祸首,但在测试过程中,我发现:
void SomeFunction(AType ofThing) {
DoSomething(ofThing);
}
。比以下速度快:
private AType _ofThing;
void SomeFunction() {
DoSomething(_ofThing);
}
我相信这与编译器如何将其转换为 CIL 有关。谁能解释一下,具体来说,为什么会发生这种情况?
下面是它发生的一些代码:
public void TestMethod1()
{
var stopwatch = new Stopwatch();
var r = new int[] { 1, 2, 3, 4, 5 };
var i = 0;
stopwatch.Start();
while (i < 1000000)
{
DoSomething(r);
i++;
}
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);
i = 0;
stopwatch.Restart();
while (i < 1000000)
{
DoSomething();
i++;
}
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);
}
private void DoSomething(int[] arg1)
{
var r = arg1[0] * arg1[1] * arg1[2] * arg1[3] * arg1[4];
}
private int[] _arg1 = new [] { 1, 2, 3, 4, 5 };
private void DoSomething()
{
var r = _arg1[0] * _arg1[1] * _arg1[2] * _arg1[3] * _arg1[4];
}
就我而言,使用私有财产要慢 2.5 倍。
我相信这与编译器如何将其转换为 CIL 有关。
没有。性能不直接取决于 CIL 代码,因为这不是实际执行的代码。执行的是 JIT 本机代码,因此当您对性能感兴趣时,应该查看它。
因此,让我们看一下为 DoSomething(int[])
循环生成的代码:
mov eax,dword ptr [ebx+4] ; get the length of the array
cmp eax,0 ; if it's 0
jbe 0000018C ; jump to code that throws IndexOutOfRangeException
cmp eax,1 ; if it's 1, etc.
jbe 0000018C
cmp eax,2
jbe 0000018C
cmp eax,3
jbe 0000018C
cmp eax,4
jbe 0000018C
inc esi ; i++
cmp esi,0F4240h ; if i < 1000000
jl 000000B7 ; loop again
这段代码的有趣之处在于根本没有做任何有用的工作,大部分代码都是数组边界检查(为什么代码没有优化为在循环之前只执行一次这种检查,我不知道(。
另请注意,代码是内联的,您无需支付函数调用的费用。
此代码在我的计算机上大约需要 1.7 毫秒。
那么,DoSomething()
的循环是什么样的呢?
mov ecx,dword ptr [ebp-10h] ; access this
call dword ptr ds:[001637F4h] ; call DoSomething()
inc esi ; i++
cmp esi,0F4240h ; if i < 1000000
jl 00000120 ; loop again
好的,所以这实际上调用了该方法,这次没有内联。方法本身是什么样的?
mov eax,dword ptr [ecx+4] ; access this._arg1
cmp dword ptr [eax+4],0 ; if its length is 0
jbe 00000022 ; jump to code that throws IndexOutOfRangeException
cmp dword ptr [eax+4],1 ; etc.
jbe 00000022
cmp dword ptr [eax+4],2
jbe 00000022
cmp dword ptr [eax+4],3
jbe 00000022
cmp dword ptr [eax+4],4
jbe 00000022
ret ; bounds checks successful, return
与以前的版本相比(并且暂时忽略函数调用的开销(,这执行了三个不同的内存访问,而不仅仅是一个,这可以解释一些性能差异。(我认为对eax+4
的五次访问应该只算作一次,否则编译器会优化它们。
这段代码对我来说大约需要 3.0 毫秒。
方法调用需要多少开销?我们可以通过将[MethodImpl(MethodImplOptions.NoInlining)]
添加到先前内联的DoSomething(int[])
来检查这一点。程序集现在如下所示:
mov ecx,dword ptr [ebp-10h] ; access this
mov edx,dword ptr [ebp-14h] ; access r
call dword ptr ds:[002937E8h] ; call DoSomething(int[])
inc esi ; i++
cmp esi,0F4240h ; if i < 1000000
jl 000000A0 ; loop again
请注意,r
现在不再保存在寄存器中,而是保存在堆栈上,这将增加另一个减速。
现在DoSomething(int[])
:
push ebp ; save ebp from caller to stack
mov ebp,esp ; write our own ebp
mov eax,dword ptr [edx+4] ; read the length of the array
cmp eax,0 ; if it's 0
jbe 00000021 ; jump to code that throws IndexOutOfRangeException
cmp eax,1 ; etc.
jbe 00000021
cmp eax,2
jbe 00000021
cmp eax,3
jbe 00000021
cmp eax,4
jbe 00000021
pop ebp ; restore ebp
ret ; return
这段代码对我来说大约需要 3.2 毫秒。这甚至比DoSomething()
还慢.这是怎么回事?
事实证明,[MethodImpl(MethodImplOptions.NoInlining)]
似乎导致了那些不必要的ebp
指令。如果我将该属性添加到 DoSomething()
,它会在 3.3 毫秒内运行。
这意味着堆栈访问和堆访问之间的差异非常小(但仍然可以测量(。当方法内联时,数组指针可以保存在寄存器中这一事实可能更为重要。
所以,结论是,你看到的最大差异是因为内联。JIT编译器决定内联DoSomething(int[])
代码,但不内联DoSomething()
,这使得DoSomething(int[])
代码非常高效。最可能的原因是因为 DoSomething()
的 IL 要长得多(21 字节与 46 字节(。
此外,你并没有真正衡量你写了什么(数组访问和乘法(,因为可以优化出来。因此,在设计微基准测试时要小心,这样编译器就不会忽略您实际想要测量的代码。
有几个人已经区分了堆栈/堆,但这是一个错误的二分法;当IL被编译为机器代码时,还有其他可能性,例如在寄存器中传递参数,这可能比将它们从堆栈中取出还要快。 请参阅Eric Lippert的精彩博客文章The Truth About Value Type,以获取有关这些思路的更多想法。 在任何情况下,对性能差异的正确分析几乎肯定需要查看生成的机器代码,而不是 IL,并且可能取决于 JIT 编译器的版本等。
如果这是你的例子,我不会惊讶地看到SomeFunction
被内联。看这里
JIT 也完全有可能无法内联第二个示例。
您需要查看编译的代码来证明这一点。我不知道一种确定性的方式来知道某些东西是否是内联栏,查看编译的代码。
您至少可以通过让另一个线程写入_ofThing
来反驳缓存,如果您得到类似的结果,同时它正在更改读取值,那么它就不会是缓存。
即使函数没有内联,由于缓存局部性,引用参数也会更快:参数已经在 CPU 缓存中。
值得注意的是,您通过调用此函数将其放入缓存中,因此已经支付了此价格。
这与变量的存储位置完全相关。如果它在堆栈上还是在堆上。下面的代码要快得多,因为它使用了static
变量,例如:
private static AType _ofThing;
void SomeFunction() {
DoSomething(_ofThing);
}
有关变量存储位置的更多信息,请查看Hans Passant的出色答案。
当您使用方法的参数调用方法时,您使用的是堆栈内存,而当使用全局变量时,您使用的是堆内存。
叠
- 非常快速的访问
- 不必显式取消分配变量
- 空间由CPU有效管理,内存不会碎片化
- 仅局部变量
- 堆栈大小限制(取决于操作系统(
- 无法调整变量大小
堆
- 变量可以全局访问
- 内存大小没有限制
- (相对(较慢的访问速度
- 无法保证有效使用空间,内存可能会随着时间的推移而碎片化,因为内存块被分配,然后释放
- 变量可以调整大小
http://tutorials.csharp-online.net/Stack_vs._Heap