大量使用 . 有任何开销吗? field/property 比较的运算符?

Is there any overhead in massively using .? operators for field/property comparisons?

如果我们有 2 个对象并且我们已经重载了一个相等运算符来比较每个对象,我们是否应该厌倦使用 ?运算符(例如 firstObject?.MyProperty == secondObject?.MyProperty)如果我们最终在 20-30 个属性上使用它?

基本上对于属性比较多的大对象,做下面的比较是不是比较好

(firstObject == null && secondObject == null || !(firstObject == null ^ secondObject == null)) 
&&  (<property comparisons without ?>)

或者干脆

<property comparisons with ?>

谢谢。

在本质上它们是 不同的?. 更具可读性并且 更快 (似乎)。

证明

首先,考虑这个样本(目标 .NET Framework 4.7.2):

class Program
{
    class Foo
    {
        public Foo Bar { get; set; }
    }

    static void Main(string[] args)
    {
        var foo = new Foo();

        var t0 = DateTime.UtcNow.Ticks;
        for (int i = 0; i < 1000000000; i++)
        {
            UsingElvisOperator(foo);
            // UsingIfsAndButs(foo);
        }
        var t1 = DateTime.UtcNow.Ticks;

        Console.WriteLine($"Elapsed time: {(t1 - t0) / 10000} ms");
    }

    private static void UsingIfsAndButs(Foo foo)
    {
        if (foo?.Bar?.Bar?.Bar != null)
        {
            Console.WriteLine("OK");
        }
    }

    private static void UsingElvisOperator(Foo foo)
    {
        if (foo != null && foo.Bar != null && foo.Bar.Bar != null && foo.Bar.Bar.Bar != null)
        {
            Console.WriteLine("OK");
        }
    }
}

Any CPU 平台的 Release 模式下编译它。现在让我们使用 ildasm.

反编译生成的 .exe

猫王运算符的说明:

.method private hidebysig static void  UsingElvisOperator(class OperatorTest.Program/Foo foo) cil managed
{
  // Code size       53 (0x35)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  brfalse.s  IL_0034
  IL_0003:  ldarg.0
  IL_0004:  callvirt   instance class OperatorTest.Program/Foo OperatorTest.Program/Foo::get_Bar()
  IL_0009:  brfalse.s  IL_0034
  IL_000b:  ldarg.0
  IL_000c:  callvirt   instance class OperatorTest.Program/Foo OperatorTest.Program/Foo::get_Bar()
  IL_0011:  callvirt   instance class OperatorTest.Program/Foo OperatorTest.Program/Foo::get_Bar()
  IL_0016:  brfalse.s  IL_0034
  IL_0018:  ldarg.0
  IL_0019:  callvirt   instance class OperatorTest.Program/Foo OperatorTest.Program/Foo::get_Bar()
  IL_001e:  callvirt   instance class OperatorTest.Program/Foo OperatorTest.Program/Foo::get_Bar()
  IL_0023:  callvirt   instance class OperatorTest.Program/Foo OperatorTest.Program/Foo::get_Bar()
  IL_0028:  brfalse.s  IL_0034
  IL_002a:  ldstr      "OK"
  IL_002f:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0034:  ret
} // end of method Program::UsingElvisOperator

以及 if-checks 的说明:

.method private hidebysig static void  UsingIfsAndButs(class OperatorTest.Program/Foo foo) cil managed
{
  // Code size       49 (0x31)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  brtrue.s   IL_0006
  IL_0003:  ldnull
  IL_0004:  br.s       IL_0024
  IL_0006:  ldarg.0
  IL_0007:  call       instance class OperatorTest.Program/Foo OperatorTest.Program/Foo::get_Bar()
  IL_000c:  dup
  IL_000d:  brtrue.s   IL_0013
  IL_000f:  pop
  IL_0010:  ldnull
  IL_0011:  br.s       IL_0024
  IL_0013:  call       instance class OperatorTest.Program/Foo OperatorTest.Program/Foo::get_Bar()
  IL_0018:  dup
  IL_0019:  brtrue.s   IL_001f
  IL_001b:  pop
  IL_001c:  ldnull
  IL_001d:  br.s       IL_0024
  IL_001f:  call       instance class OperatorTest.Program/Foo OperatorTest.Program/Foo::get_Bar()
  IL_0024:  brfalse.s  IL_0030
  IL_0026:  ldstr      "OK"
  IL_002b:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0030:  ret
} // end of method Program::UsingIfsAndButs

很明显,生成的指令是不同的。

性能比较

运行 两种方法 10 亿次 次,在我的机器上,得到以下结果:

UsingElvisOperator:   1704 ms
UsingIfsAndButs:      2815 ms

只想分享我的基准测试总结,它检查 null 和非 null 测试对象,这显示了“ifs”的速度优势:

// * Summary *
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.900 (1909/November2018Update/19H2)
Intel Core i5-7300U CPU 2.60GHz (Kaby Lake), 1 CPU, 4 logical and 2 physical cores
.NET Core SDK=3.1.301

  [Host] : .NET Core 3.1.5 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.27001), X64 RyuJIT

Job=NewJob  IterationCount=2  LaunchCount=2  
RunStrategy=Throughput  WarmupCount=1  

|             Method |     Mean |     Error |    StdDev |
|
------------------- |---------:|----------:|----------:|
|    UsingIfsAndButs | 1.518 ns | 0.4293 ns | 0.0664 ns |
| UsingElvisOperator | 2.144 ns | 0.2124 ns | 0.0329 ns |

和代码:

[SimpleJob(RunStrategy.Throughput, launchCount: 2, warmupCount: 1, targetCount: 2, id: "NewJob")]
public class ElvisBenchmarks
{
    Foo foo;
    Foo foo_null;
    Boolean isNull;
    
    public ElvisBenchmarks()
    {
         foo = new Foo();
    }

    [Benchmark]
    public void UsingIfsAndButs()
    {
        isNull = (foo != null && foo.Bar != null && foo.Bar.Bar != null && foo.Bar.Bar.Bar != null)
                ? true : false;

        isNull = (foo_null != null && foo_null.Bar != null && foo_null.Bar.Bar != null && foo_null.Bar.Bar.Bar != null)
                ? true : false;
    }
        
    [Benchmark]
    public void UsingElvisOperator()
    {
        isNull = (foo?.Bar?.Bar?.Bar != null) ? true : false;
        isNull = (foo_null?.Bar?.Bar?.Bar != null) ? true : false;
    }
}

class Foo
{
    public Foo Bar { get; set; }
}