Panzercrisis Panzercrisis - 1 month ago 6
C# Question

In VB.NET, C#, etc., does the one-dot rule automatically get optimized into the code?

In VB.NET, C#, etc., something like this wouldn't get optimized, would it?

a.B.C.X = 5

Dim cachedReference As ClassOfC = a.B.C
cachedReference.X = 5

In ECMA-type languages at least, this is a good habit to be in to minimize how many times it goes to
, then goes to its
field/property, then finally goes to its
field/property. I would think that is normally the rule of thumb in any typical object-oriented or procedural language, except where there's at least a pretty solid expectation that it's just going to get optimized like that by the compiler/jit/etc. anyway. How is this handled in typical .NET, particularly for VB.NET and C#?

This, for instance, doesn't seem to mention the one-dot rule, at least pertaining to either of those two languages: On the other hand, I would be very surprised if it did this in general for properties, since those are effectively methods. It would seem to make more sense if it did this if and only if they were fields are possibly even completely trivial properties.


Okay, I went ahead and tried it out for curiosity's sake.

Given the following classes:

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

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

public class Baz
    public string One { get; set; } = string.Empty;
    public string Two { get; set; } = string.Empty;
    public bool BothPopulated() => !(string.IsNullOrWhiteSpace(One) || string.IsNullOrWhiteSpace(Two));

public class FooFactory
    public static Foo Create() => new Foo { Bar = new Bar { Baz = new Baz { One = "Hello", Two = "World" } } };

And the following method:

void Main()
    var foo = FooFactory.Create();
    var cached = foo.Bar.Baz;

    var fooTwo = FooFactory.Create();

LinqPad reports the IL emitted for main in release mode as

IL_0000:  call        UserQuery+FooFactory.Create
IL_0005:  callvirt    UserQuery+Foo.get_Bar
IL_000A:  callvirt    UserQuery+Bar.get_Baz
IL_000F:  dup         
IL_0010:  callvirt    UserQuery+Baz.get_One
IL_0015:  call        System.Console.WriteLine
IL_001A:  dup         
IL_001B:  callvirt    UserQuery+Baz.get_Two
IL_0020:  call        System.Console.WriteLine
IL_0025:  callvirt    UserQuery+Baz.BothPopulated
IL_002A:  call        System.Console.WriteLine // <- End of cached portion
IL_002F:  call        UserQuery+FooFactory.Create
IL_0034:  dup         
IL_0035:  callvirt    UserQuery+Foo.get_Bar
IL_003A:  callvirt    UserQuery+Bar.get_Baz
IL_003F:  callvirt    UserQuery+Baz.get_One
IL_0044:  call        System.Console.WriteLine 
IL_0049:  dup         
IL_004A:  callvirt    UserQuery+Foo.get_Bar
IL_004F:  callvirt    UserQuery+Bar.get_Baz
IL_0054:  callvirt    UserQuery+Baz.get_Two
IL_0059:  call        System.Console.WriteLine
IL_005E:  callvirt    UserQuery+Foo.get_Bar
IL_0063:  callvirt    UserQuery+Bar.get_Baz
IL_0068:  callvirt    UserQuery+Baz.BothPopulated
IL_006D:  call        System.Console.WriteLine
IL_0072:  ret 

So it looks like you do save some callvirts by not drilling through the properties each time. Whether this has any measurable impact on the JIT at run-time is not something I can answer, but it does appear to leave a smaller IL footprint.

I suspect it has basically zero impact on run-time performance.