Does accessing a variable in C# class reads the entire class from memory?

Viewed 630

I am pretty new to c# and I have one question the bothers me for a while.

When I learned C# I was taught that a class shouldn't contain a lot of variables because then reading a variable (or invoking a method from it) would be slow.

I was told that when I access a variable in class C# it would read the entire class from memory in order to read the variable data, but it sounds weird and wrong to me.

For example, if I have this class:

public class Test
{
    public int toAccess; // 32 bit
    private byte someValue; // 8 bit
    private short anotherValue; // 16 bit
} 

Then accessing it from main:

public class MainClass
{
    private Test test;
    public MainClass(Test test)
    {
        this.test = test;
    }
    public static void Main(string[] args)
    {
        var main = new MainClass(new Test());
        Console.WriteLine(main.test.toAccess); // Would read all 56 bit of the class
    }
}

My questions is: Is it actually true¿ Does the entire class is read when accessing a variable¿

2 Answers

For classes, this literally doesn't make any difference; you're always just dealing with a reference and an offset from that reference. Passing a reference is plenty cheap.

When it does start to matter is with structs. Note that this doesn't impact calling methods on the type - that is a ref-based static call, typically; but when the struct is a parameter to a method, it matters.

(edit: actually, it also matters when calling methods on structs if you're calling them via a boxing operation, as a box is also a copy; that's a great reason to avoid boxed calls!)

Disclaimer: you probably shouldn't be routinely using structs.

For structs, the value takes that much space wherever it is used as a value, which could be as a field, a local on the stack, a parameter to a method, etc. This also means that copying the struct (for example, to pass as a parameter) can be expensive. But if we take an example:

struct MyBigStruct {
   // lots of fields here
}

void Foo() {
    MyBigStruct x = ...
    Bar(x);
}
void Bar(MyBigStruct s) {...}

then at the point when we call Bar(x), we copy the struct on the stack. Likewise whenever a local is used for storage (assuming it isn't boiled away by the compiler):

MyBigStruct x = ...
MyBigStruct asCopy = x;

But! we can fix these things... by passing a reference around instead. In current versions of C#, this is done most appropriately using in, ref readonly, and readonly struct:

readonly struct MyBigStruct {
   // lots of readonly fields here
}
void Foo() {
    MyBigStruct x = ...
    Bar(x); // note that "in" is implicit when needed, unlike "ref" or "out"
    ref readonly MyBigStruct asRef = ref x;
}
void Bar(in MyBigStruct s) {...}

Now there are zero actual copies. Everything here is dealing with references to the original x. The fact that it is readonly means that the runtime knows that it can trust the in declaration on the parameter, without needing a defensive copy of the value.

Ironically, perhaps: adding the in modifier on a parameter can introduce copying if the input type is a struct that is not marked readonly, as the compiler and runtime need to guarantee that changes made inside Bar won't be visible to the caller. Those changes need not be obvious - any method call (which includes property getters and some operators) can mutate the value, if the type is evil. As an evil example:

struct Evil
{
    private int _count;
    public int Count => _count++;
}

The job of the compiler and runtime is to work predictably even if you are evil, hence it adds a defensive copy of the struct. The same code with the readonly modifier on the struct will not compile.


You can also do something similar to in with ref if the type isn't readonly, but then you need to be aware that if Bar mutates the value (deliberately or as a side-effect), those changes will be visible to Foo.

Short answer

No.

Less short answer

The compiler creates members tables when it creates intermediate language code (the .NET assembly language or IL) and when you access a class member, it indicates in the code the exact offset to be added to the reference (the memory base address of the instance) of this member.

For example (in simplified shorthand), if the reference of an instance of an object is at the memory address 0x12345600, and the offset of a member int Value is 0x00000010, then the CLR will get an instruction to execute to fetch the contents of the zone in 0x12345610.

So it is not needed to parse the entire class structure in the memory.

Long answer

Here is the IL code of your Main method from ILSpy:

// Method begins at RVA 0x2e64
// Code size 30 (0x1e)
.maxstack 1
.locals init (
  [0] class ConsoleApp.Program/MainClass main
)

// (no C# code)
IL_0000: nop
// MainClass mainClass = new MainClass(new Test());
IL_0001: newobj instance void ConsoleApp.Program/Test::.ctor()
IL_0006: newobj instance void ConsoleApp.Program/MainClass::.ctor(class ConsoleApp.Program/Test)
IL_000b: stloc.0
// Console.WriteLine(mainClass.test.toAccess);
IL_000c: ldloc.0
IL_000d: ldfld class ConsoleApp.Program/Test ConsoleApp.Program/MainClass::test
IL_0012: ldfld int32 ConsoleApp.Program/Test::toAccess
IL_0017: call void [mscorlib]System.Console::WriteLine(int32)
// (no C# code)
IL_001c: nop
// }
IL_001d: ret

As you can see, the WriteLine instruction get the value to write using:

IL_000d: ldfld class ConsoleApp.Program/Test ConsoleApp.Program/MainClass::test

=> Here it loads the base memory address of the test instance (a reference is an hidden pointer to forget to manage it)

IL_0012: ldfld int32 ConsoleApp.Program/Test::toAccess

=> Here it loads the offset of the memory address of the toAccess field.

Next the WriteLine is called by passing the parameter needed that is the content of the base + offset Int32 memory zone: the value is pushed in the stack (ldfld) and the called method will pop this stack to get the parameter (ldarg).

In the WriteLine you will have this instruction to get the parameter value:

ldarg.1
Related