为什么在 C# 中,从函数参数引用变量比从私有属性引用变量更快


可能是

我的硬件是罪魁祸首,但在测试过程中,我发现:

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

相关内容

  • 没有找到相关文章

最新更新