3

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¿

EylM
  • 5,967
  • 2
  • 16
  • 28
SagiZiv
  • 932
  • 1
  • 16
  • 38
  • 2
    its a method call for properties. the values are already in memory there so the clr will just read that one value from memory – Daniel A. White Oct 09 '19 at 15:05
  • 2
    I think you've mixed up a few concepts. The class is already in memory, so that's no issue. It is true, however, that when you want to access a property, the whole class gets loaded into the CPU cache which might cause cache misses. – Filip Vondrášek Oct 09 '19 at 15:11
  • 1
    As a general rule, you should not worry about micro-optimizations, such as worrying about the performance of accessing variables from classes and memory. Instead, think about algorithm performance when the data set has the potential to be very large (e.g. millions of records), and benchmark performance when things slow down to potentially optimize those small parts of the program. That's it. Beyond those situations, code for readability and maintainability alone. – Slothario Oct 09 '19 at 15:17
  • 2
    You were told a large number of very wrong or misleading things apparently. I don't understand what you mean by "read the class into memory". Can you explain? You seem to be concerned about the time it takes to do things; your best course of action is to *measure* the amount of time a thing takes if you are concerned about it. But the most important thing here is: you've been learning about C# from someone who is teaching you falsehoods. Find a better teacher! – Eric Lippert Oct 09 '19 at 15:39
  • @FilipVondrášek Yeah it's my bad, I am still trying to learn the terms and the differences between them. – SagiZiv Oct 09 '19 at 15:58
  • @Slothario I know that I should optimize my code only when necessary, but I always try to get to the "perfect code" that is highly optimized (It would never happen, but I can't seem to help it ). – SagiZiv Oct 09 '19 at 16:01
  • 3
    @SagiZiv: Then you're optimizing for the wrong thing. Remember, when you are writing code professionally the goal is not to make the fastest code or the shortest code. It is to make *the code that produces the most value for the least cost*. Spending an hour of coding making an operation that takes 10000000 nanoseconds two nanoseconds faster is a bad use of time. Spending an hour of coding making an operation 0.0001% slower but more robust against failure on the user's machine is a good use of time. – Eric Lippert Oct 09 '19 at 20:14
  • @SagiZiv If you're interested in optimizing things I would recommend doing a side project where performance is critical and really measure the heck out of everything to get a sense of what matters and what doesn't. When performance doesn't matter (e.g. the piece of code you work on takes only 1% of total processing time), code for readability. Otherwise you're kind of "cargo cult" programming and not basing assumptions on actual data or experience. I don't mean to sound rude -- it's an easy trap to fall into! – Slothario Oct 10 '19 at 18:34

2 Answers2

9

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.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Thank you for the great and detailed answer, it really helps me. I guess I need to do some more research about C#. – SagiZiv Oct 09 '19 at 15:55
  • "... adding the `in` modifier on a parameter can _introduce_ copying if the input type is a `struct` that is not marked `readonly` ...". But if you do not use the `in` modifier, wouldn't the struct be copied anyway? – Luca Cremonesi Oct 10 '19 at 15:12
  • 1
    @LucaCremonesi true, indeed; I guess what I mean is: "don't *assume* that `in` always avoids the copy" - in fact, just the other day I changed some "`in this`" extension methods to "`ref this`", because the `struct` in questions wasn't `readonly` on all my targeted TFMs (`ArraySegment`, IIRC) – Marc Gravell Oct 10 '19 at 15:40
  • What goes for "big struct" nowadays? 128 bytes and larger? – Joker_vD Oct 10 '19 at 19:37
  • @Joker_vD depends on how "hot" a path it is, and what measurement shows – Marc Gravell Oct 10 '19 at 19:53
6

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
  • Thanks for the detailed answer, it really helped. I didn't know that C# works this way, I am going to do some more research about it. Thanks again for the great answer – SagiZiv Oct 09 '19 at 15:53