为什么 shorthand 访问器函数比它们的常规对应函数更快?

Why are shorthand accessor functions faster than their regular counterparts?

我运行测试了下面两个访问函数的速度差异,时间差异比我预期的要大。我只是觉得 shorthand 实施可能会快一点,所以我想测试一下。

我测量了为每个 class、10 亿次迭代调用 Get 函数所需的总秒数。

using System;
using System.Diagnostics;

class SimpleGet {
    int value;

    public int Get() {
        return value;
    }
}

class ShorthandGet {
    int value;

    public int Get() => value;
}

class Program {
    static void Main() {
        const int Iterations = 1000000000;
        Stopwatch sw = new Stopwatch();

        sw.Start();
        {
            int n; SimpleGet sg = new SimpleGet();
            for (int i = 0; i < Iterations; i++) {
                n = sg.Get();
            }
        }
        sw.Stop();
        Console.WriteLine("SimpleGet: " + sw.Elapsed.TotalSeconds);

        sw.Reset();

        sw.Start();
        {
            int n; ShorthandGet shg = new ShorthandGet();
            for (int i = 0; i < Iterations; i++) {
                n = shg.Get();
            }
        }
        sw.Stop();
        Console.WriteLine("ShorthandGet: " + sw.Elapsed.TotalSeconds);

        Console.ReadLine();

    }
}

结果:

// 1 billion iterations
SimpleGet: 11.8484244
ShorthandGet: 4.3218568

速度差别很大。我能看到的唯一区别是常规函数有括号,因此在函数调用时创建了一个新的范围。由于范围内没有新变量,理论上不应该 "disregarded" 有人可以解释为什么常规函数没有被优化到与另一个相同的水平吗?

编辑

我用属性测试了相同的场景:Value { get { return value; } }Value => value; 并且时间差非常接近各自的函数时间差。我想原因是一样的。

简短的回答是正确完成基准测试没有区别。

对于像这样的微优化案例,我总是喜欢先看一下 IL。不是因为您会获得一些深刻的见解,而是因为如果生成相同的 IL,那么在 运行 时间应该没有区别。接下来要记住的是,您必须从发布版本开始,因为编译器会删除这些版本中不必要的 IL 指令。

在调试版本中,长格式 IL (SimpleGet) 有额外的指令来启用放置断点:

.method public hidebysig 
    instance int32 Get () cil managed 
{
    IL_0000: nop
    IL_0001: ldarg.0
    IL_0002: ldfld int32 ConsoleApplication7.SimpleGet::'value'
    IL_0007: stloc.0
    IL_0008: br.s IL_000a
    IL_000a: ldloc.0
    IL_000b: ret
}

与 ShorthandGet 相比更短:

.method public hidebysig 
    instance int32 Get () cil managed 
{
    IL_0000: ldarg.0
    IL_0001: ldfld int32 ConsoleApplication7.ShorthandGet::'value'
    IL_0006: ret
}

然而,在优化的构建中,两种形式都会生成与上述 ShorthandGet 相同的 IL。

调试版本的基准测试可能会像您所展示的那样显示差异,但这些永远不值得比较,因为如果您关心性能,您将 运行使用发布版本的优化代码。这个故事的寓意是始终对优化代码进行性能分析。另一个经常被忽略的项目是在没有附加调试器的情况下进行基准测试,因为即使对于优化的 IL,JIT 也会检测调试器并发出更多可调试的机器代码。许多人都忽略了这一点,因为他们只是在 VS 中单击 "Start" 或按 F5,但这 启动程序并附加了调试器 。使用菜单选项 Debug > Start Without Debugging。