CLR 如何优化 属性 引用?

How does CLR optimize property references?

我当了很长时间的程序员,最近找到了一份编写 C# 的工作。我很好奇是否 Visual Studio 优化了对简单内存移动的 属性 调用而不是执行函数调用和 return。所以我写了一个程序,它有两个版本的 3D 点 class 和一种计算大小的方法:一个版本直接访问字段,一个使用属性。我 运行 都获得了 100,000,000 分,他们花费的时间相同。但是当我使用 ildasm 查看生成的代码时,使用属性的版本似乎使用函数调用来访问 属性 值。 (这是发布版本,因此代码优化已打开。)

我的问题:

  1. 对 get_X 的函数调用是否针对运行时的内存移动进行了优化? (看起来是这样,因为它的执行时间与直接字段引用相同。)

  2. 有没有办法使用 ildasm 或其他工具来查看在运行时发生了哪些优化?

我已经尝试 运行 没有调试器的进程然后附加到进程但是 VS2017 说 "No disassembly available"。

直接调用私有字段的版本:

.method public hidebysig instance float64
          Abs() cil managed
{
    // Code size       47 (0x2f)
    .maxstack  8
    //000052:             return Math.Sqrt(_x * _x + _y * _y + _z * _z);
    IL_0000:  ldarg.0
    IL_0001:  ldfld      float64 CPUTests.Point3d::_x
    IL_0006:  ldarg.0
    IL_0007:  ldfld      float64 CPUTests.Point3d::_x
    IL_000c:  mul
    IL_000d:  ldarg.0
    IL_000e:  ldfld      float64 CPUTests.Point3d::_y
    IL_0013:  ldarg.0
    IL_0014:  ldfld      float64 CPUTests.Point3d::_y
    IL_0019:  mul
    IL_001a:  add
    IL_001b:  ldarg.0
    IL_001c:  ldfld      float64 CPUTests.Point3d::_z
    IL_0021:  ldarg.0
    IL_0022:  ldfld      float64 CPUTests.Point3d::_z
    IL_0027:  mul
    IL_0028:  add
    IL_0029:  call       float64 [mscorlib]System.Math::Sqrt(float64)
    IL_002e:  ret
} // end of method Point3d::Abs

调用属性的版本,后面是get_X方法:

.method public hidebysig instance float64
          Abs() cil managed
{
    // Code size       47 (0x2f)
    .maxstack  8
    //000052:             return Math.Sqrt(X * X + Y * Y + Z * Z);
    IL_0000:  ldarg.0
    IL_0001:  call       instance float64 CPUTests.Point3dProperties::get_X()
    IL_0006:  ldarg.0
    IL_0007:  call       instance float64 CPUTests.Point3dProperties::get_X()
    IL_000c:  mul
    IL_000d:  ldarg.0
    IL_000e:  call       instance float64 CPUTests.Point3dProperties::get_Y()
    IL_0013:  ldarg.0
    IL_0014:  call       instance float64 CPUTests.Point3dProperties::get_Y()
    IL_0019:  mul
    IL_001a:  add
    IL_001b:  ldarg.0
    IL_001c:  call       instance float64 CPUTests.Point3dProperties::get_Z()
    IL_0021:  ldarg.0
    IL_0022:  call       instance float64 CPUTests.Point3dProperties::get_Z()
    IL_0027:  mul
    IL_0028:  add
    IL_0029:  call       float64 [mscorlib]System.Math::Sqrt(float64)
    IL_002e:  ret
} // end of method Point3dProperties::Abs

.method public hidebysig specialname instance float64
          get_X() cil managed
{
    // Code size       7 (0x7)
    .maxstack  8
    //000016:             get { return _x; }
    IL_0000:  ldarg.0
    IL_0001:  ldfld      float64 CPUTests.Point3dProperties::_x
    IL_0006:  ret
} // end of method Point3dProperties::get_X

Jeffrey Richter 在他的《通过 C# 实现 CLR》一书中写道:

For simple get and set accessor methods, the just-in-time (JIT) compiler inlines the code so that there’s no run-time performance hit as a result of using properties rather than fields.

通过 C# 的 CLR 是我的圣经,所以对我来说,这足以证明它们是内联的。

ildasm 只能向您显示编译时发生的优化的最终结果。如果要检查 运行 时间优化,则必须实际查看 运行 时间代码,即从程序集生成并正在 运行 的代码。 S.O.S WinDbg 扩展可能是一个可以帮助您的工具。

调试时使用反汇编视图可能会显示 属性 getter 和 setter 确实是内联的。 您可以在 visual studio 中使用 CTRL-ALT-D 访问它。

如果您想发现抖动优化,可以使用 MethodImplOptions 禁用它们。

public struct foo
    {
        private int _bar;
        public int bar
        {
            [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
            get { return _bar; }
            [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
            set { _bar = value; }
        }
    }

将在调试时向您展示:

而如果你不放属性,你连断点都打不到,因为所有的getter都被内联了。