Pull to refresh
0
Семинары Станислава Сидристого
CLRium #7: Parallel Computing Practice

.NET Reference Types vs Value Types. Part 1

Reading time 16 min
Views 7K

First, let’s talk about Reference Types and Value Types. I think people don’t really understand the differences and benefits of both. They usually say reference types store content on the heap and value types store content on the stack, which is wrong.


Let’s discuss the real differences:


  • A value type: its value is an entire structure. The value of a reference type is a reference to an object. – A structure in memory: value types contain only the data you indicated. Reference types also contain two system fields. The first one stores 'SyncBlockIndex', the second one stores the information about a type, including the information about a Virtual Methods Table (VMT).
  • Reference types can have methods that are overridden when inherited. Value types cannot be inherited.
  • You should allocate space on the heap for an instance of a reference type. A value type can be allocated on the stack, or it becomes the part of a reference type. This sufficiently increases the performance of some algorithms.

However, there are common features:


  • Both subclasses can inherit the object type and become its representatives.

Let’s look closer at each feature.


This chapter was translated from Russian jointly by author and by professional translators. You can help us with translation from Russian or English into any other language, primarily into Chinese or German.

Also, if you want thank us, the best way you can do that is to give us a star on github or to fork repository github/sidristij/dotnetbook.


Let’s look closer at each feature.


Copying


The main difference between the two types is as follows:


  • Each variable, class or structure fields or method parameters that take a reference type store a reference to a value;
  • But each variable, class or structure fields or method parameters that take a value type store a value exactly, i.e. an entire structure.

This means that assigning or passing a parameter to a method will copy the value. Even if you change the copy, the original will stay the same. However, if you change reference type fields, this will “affect” all parts with a reference to an instance of a type. Let’s look at the
example:


DateTime dt = DateTime.Now;   // Here, we allocate space for DateTime variable when calling a method,
                              // but it will contain zeros. Next, let’s copy all 
                              // values of the Now property to dt variable
DateTime dt2 = dt;            // Here, we copy the value once again

object obj = new object();    // Here, we create an object by allocating memory on the Small Object Heap,
                              // and put a pointer to the object in obj variable
object obj2 = obj;            // Here, we copy a reference to this object. Finally, 
                              // we have one object and two references.

It seems this property produces ambiguous code constructs such as the
change of code in collections:


// Let’s declare a structure
struct ValueHolder
{
    public int Data;
}

// Let’s create an array of such structures and initialize the Data field = 5
var array = new [] { new ValueHolder { Data = 5 } };

// Let’s use an index to get the structure and put 4 in the Data field
array[0].Data = 4;

// Let’s check the value
Console.WriteLine(array[0].Data);

There is a small trick in this code. It looks like that we get the structure instance first, and then assign a new value to the Data field of the copy. This means we should get 5 again when checking the value. However, this doesn't happen. MSIL has a separate instruction for setting the values of fields in the structures of an array, which increases performance. The code will work as intended: the program will
output 4 to a console.


Let’s see what will happen if we change this code:


// Let’s declare a structure
struct ValueHolder
{
    public int Data;
}

// Let’s create a list of such structures and initialize the Data field = 5
var list = new List<ValueHolder> { new ValueHolder { Data = 5 } };

// Let’s use an index to get the structure and put 4 in the Data field
list[0].Data = 4;

// Let’s check the value
Console.WriteLine(list[0].Data);

The compilation of this code will fail, because when you write list[0].Data = 4 you get the copy of the structure first. In fact, you are calling an instance method of the List<T> type that underlies the access by an index. It takes the copy of a structure from an internal array (List<T> stores data in arrays) and returns this copy to you from the access method using an index. Next, you try to modify the copy, which is not used further along. This code is pointless. A compiler prohibits such behavior, knowing that people misuse value types. We should rewrite this example in the following way:


// Let’s declare a structure
struct ValueHolder
{
    public int Data;
}

// Let’s create a list of such structures and initialize the Data field = 5
var list = new List<ValueHolder> { new ValueHolder { Data = 5 } };

// Let’s use an index to get the structure and put 4 in the Data field. Then, let’s save it again.
var copy = list[0];
copy.Data = 4;
list[0] = copy;

// Let’s check the value
Console.WriteLine(list[0].Data);

This code is correct despite its apparent redundancy. The program will
output 4 to a console.


The next example shows what I mean by “the value of a structure is an
entire structure”


// Variant 1
struct PersonInfo
{
    public int Height;
    public int Width;
    public int HairColor;
}

int x = 5;
PersonInfo person;
int y = 6;

// Variant 2

int x = 5;
int Height;
int Width;
int HairColor;
int y = 6;

Both examples are similar in terms of the data location in memory, as the value of the structure is the entire structure. It allocates the memory for itself where it is.


// Variant 1
struct PersonInfo
{
    public int Height;
    public int Width;
    public int HairColor;
}

class Employee
{
    public int x;
    public PersonInfo person;
    public int y;
}

// Variant 2
class Employee
{
    public int x;
    public int Height;
    public int Width;
    public int HairColor;
    public int y;
}

These examples are also similar in terms of the elements’ location in memory as the structure takes a defined place among class fields. I don’t say they are totally similar as you can operate structure fields using structure methods.


Of course, this is not the case of reference types. An instance itself is on the unreachable Small Object Heap (SOH) or the Large Object Heap (LOH). A class field only contains the value of a pointer to an instance: a 32 or 64-bit number.


Let’s look at the last example to close the issue.


// Variant 1
struct PersonInfo
{
    public int Height;
    public int Width;
    public int HairColor;
}

void Method(int x, PersonInfo person, int y);

// Variant 2
void Method(int x, int HairColor, int Width, int Height, int y);

In terms of memory both variants of code will work in a similar way, but not in terms of architecture. It is not just a replacement of a variable number of arguments. The order changes because method parameters are declared one after another. They are put on the stack the similar way.


However, the stack grows from higher to lower addresses. That means the order of pushing a structure piece by piece will be different from pushing it as a whole.


Overridable methods and inheritance


The next big difference between the two types is the lack of virtual
methods table in structures. This means that:


  1. You cannot describe and override virtual methods in structures.
  2. A structure cannot inherit another one. The only way to emulate inheritance is to put a base type structure in the first field. The fields of an “inherited” structure will go after the fields of a “base” structure and it will create logical inheritance. The fields of both structures will coincide based on the offset.
  3. You can pass structures to unmanaged code. However, you will lose the information about methods. This is because a structure is just space in memory, filled with data without the information about a type. You can pass it to unmanaged methods, for example, written in C++, without changes.

The lack of a virtual methods table subtracts a certain part of inheritance “magic” from structures but gives them other advantages. The first one is that we can pass instances of such a structure to external environments (outside .NET Framework). Remember, this is just a memory
range! We can also take a memory range from unmanaged code and cast a type to our structure to make its fields more accessible. You cannot do this with classes as they have two inaccessible fields. These are SyncBlockIndex and a virtual methods table address. If those two fields pass to unmanaged code, it will be dangerous. Using a virtual methods table one can access any type and change it to attack an application.


Let’s show it is just a memory range without additional logic.


unsafe void Main()
{
    int secret = 666;
    HeightHolder hh;
    hh.Height = 5;
    WidthHolder wh;
    unsafe
    {
        // This cast wouldn’t work if structures had the information about a type.
        // The CLR would check a hierarchy before casting a type and if it didn’t find WidthHolder,
        // it would output an InvalidCastException exception. But since a structure is a memory range,
        // you can interpret it as any kind of structure.
        wh = *(WidthHolder*)&hh;
   }
   Console.WriteLine("Width: " + wh.Width);
   Console.WriteLine("Secret:" + wh.Secret);
}

struct WidthHolder
{
    public int Width;
    public int Secret;
}

struct HeightHolder
{
    public int Height;
}

Here, we perform the operation that is impossible in strong typing. We cast one type to another incompatible one that contains one extra field. We introduce an additional variable inside the Main method. In theory, its value is secret. However, the example code will output the value of a variable, not found in any of the structures inside the Main() method. You might consider it a breach in security, but things are not so simple. You cannot get rid of unmanaged code in a program. The main reason is the structure of the thread stack. One can use it to access unmanaged code and play with local variables. You can defend your code from these attacks by randomizing the size of a stack frame. Or, you can delete the information about EBP register to complicate the return of a stack frame. However, this doesn't matter for us now. What we are interested in this example is the following. The "secret" variable goes before the definition of hh variable and after it in WidthHolder structure (in different places, actually). So why did we easily get its value? The answer is that stack grows from right to left. The variables declared first will have much higher addresses, and those declared later will have lower addresses.


The behavior when calling instance methods


Both data types have another feature which is not plain to see and can explain the structure of both types. It deals with calling instance methods.



// The example with a reference type
class FooClass
{
    private int x;

    public void ChangeTo(int val)
    {
        x = val;
    }
}

// The example with a value type
struct FooStruct
{
    private int x;
    public void ChangeTo(int val)
    {
        x = val;
    }
}

FooClass klass = new FooClass();
FooStruct strukt = new FooStruct();

klass.ChangeTo(10);
strukt.ChangeTo(10);

Logically, we can decide that the method has one compiled body. In other words, there is no instance of a type that has its own compiled set of methods, similar to the sets of other instances. However, the called method knows which instance it belongs to as a reference to the instance of a type is the first parameter. We can rewrite our example and it will be identical to what we said before. I’m not using an example with virtual methods deliberately, as they have another procedure.



// An example with a reference type
class FooClass
{
    public int x;
}

// An example with a value type
struct FooStruct
{
    public int x;
}
public void ChangeTo(FooClass klass, int val)
{
    klass.x = val;
}

public void ChangeTo(ref FooStruct strukt, int val)
{
    strukt.x = val;
}

FooClass klass = new FooClass();
FooStruct strukt = new FooStruct();

ChangeTo(klass, 10);
ChangeTo(ref strukt, 10);

I should explain the use of the ref keyword. If I didn’t use it, I would get a copy of the structure as a method parameter instead of the original. Then I would change it, but the original would stay the same. I would have to return a changed copy from a method to a caller (another copying), and the caller would save this value back in the variable (one more copying). Instead, an instance method gets a pointer and use it for changing the original straight away. Using a pointer doesn’t influence performance as any processor-level operations use pointers. Ref is a part of the C# world, no more.


The capability to point to the position of elements.


Both structures and classes have another capability to point to the offset of a particular field in respect to the beginning of a structure in memory. This serves several purposes:


  • to work with external APIs in the unmanaged world without having to insert unused fields before a necessary one;
  • to instruct a compiler to locate a field right at the beginning of the ([FieldOffset(0)]) type. It will make the work with this type faster. If it is a frequently used field, we can increase application's performance. However, this is true only for value types. In reference types the field with a zero offset contains the address of a virtual methods table, which takes 1 machine word. Even if you address the first field of a class, it will use complex addressing (address + offset). This is because the most used class field is the address of a virtual methods table. The table is necessary to call all the virtual methods;
  • to point to several fields using one address. In this case, the same value is interpreted as different data types. In C++ this data type is called a union;
  • not to bother to declare anything: a compiler will allocate fields optimally. Thus, the final order of fields may be different.

General remarks


  • Auto: the run-time environment automatically chooses a location and a packing for all class or structure fields. The defined structures that are marked by a member of this enumeration cannot pass into unmanaged code. The attempt to do it will produce an exception;
  • Explicit: a programmer explicitly controls the exact location of each field of a type with the FieldOffsetAttribute;
  • Sequential: type members come in a sequential order, defined during type design. The StructLayoutAttribute.Pack value of a packing step indicates their location.

Using FieldOffset to skip unused structure fields


The structures coming from the unmanaged world can contain reserved fields. One can use them in a future version of a library. In C/C++ we fill these gaps by adding fields, e.g. reserved1, reserved2,… However, in .NET we just offset to the beginning of a field by using the FieldOffsetAttribute attribute and [StructLayout(LayoutKind.Explicit)].


[StructLayout(LayoutKind.Explicit)]
public struct SYSTEM_INFO
{
    [FieldOffset(0)] public ulong OemId;

    // 92 bytes reserved
    [FieldOffset(100)] public ulong PageSize;
    [FieldOffset(108)] public ulong ActiveProcessorMask;
    [FieldOffset(116)] public ulong NumberOfProcessors;
    [FieldOffset(124)] public ulong ProcessorType;
}

A gap is occupied but unused space. The structure will have the size equal to 132 and not 40 bytes as it may seem from the beginning.


Union


Using the FieldOffsetAttribute you can emulate the C/C++ type called a union. It allows to access the same data as entities of
different types. Let’s look at the example:


// If we read the RGBA.Value, we will get an Int32 value accumulating all
// other fields.
// However, if we try to read the RGBA.R, RGBA.G, RGBA.B, RGBA.Alpha, we 
// will get separate components of Int32.
[StructLayout(LayoutKind.Explicit)]
public struct RGBA
{
    [FieldOffset(0)] public uint Value;
    [FieldOffset(0)] public byte R;
    [FieldOffset(1)] public byte G;
    [FieldOffset(2)] public byte B;
    [FieldOffset(3)] public byte Alpha;
}

You might say such behavior is possible only for value types. However, you can simulate it for reference types, using one address for overlapping two reference types or one reference type and one value type:


class Program
{
    public static void Main()
    {
        Union x = new Union();
        x.Reference.Value = "Hello!";
        Console.WriteLine(x.Value.Value);
    }

    [StructLayout(LayoutKind.Explicit)]
    public class Union
    {
        public Union()
        {
            Value = new Holder<IntPtr>();
            Reference = new Holder<object>();
        }

        [FieldOffset(0)]
        public Holder<IntPtr> Value;

        [FieldOffset(0)]
        public Holder<object> Reference;
    }

    public class Holder<T>
    {
        public T Value;
    }
}

I used a generic type for overlapping on purpose. If I used usual
overlapping, this type would cause the TypeLoadException when loaded in an application domain. It might look like a security breach in theory (especially, when talking about application plug-ins), but if we try to run this code using a protected domain, we will get the same TypeLoadException.


The difference in allocation


Another feature that differentiates both types is memory allocation for objects or structures. The CLR must decide on several things before allocating memory for an object. What is the size of an object? Is it more or less than 85K? If it is less, then is there enough free space on the SOH to allocate this object? If it is more, the CLR activates Garbage Collector. It goes through an object graph, compacts the objects by moving them to cleared space. If there is still no space on the SOH, the allocation of additional virtual memory pages will start. It is only then that an object gets allocated space, cleared from garbage. Afterwards, the CLR lays out SyncBlockIndex and VirtualMethodsTable. Finally, the reference to an object returns to a user.


If an allocated object is bigger than 85K, it goes to the Large Objects Heap (LOH). This is the case of large strings and arrays. Here, we must find the most suitable space in memory from the list of unoccupied ranges or allocate a new one. It is not quick, but we are going to deal with the objects of such size carefully. Also, we are not going to talk about them here.


There are several possible scenarios for RefTypes:


  • RefType < 85K, there is space on the SOH: quick memory allocation;
  • RefType < 85K, the space on the SOH is running out: very slow memory allocation;
  • RefType > 85K, slow memory allocation.

Such operations are rare and can’t compete with ValTypes. The algorithm of memory allocation for value types doesn’t exist. The allocation of memory for value types costs nothing. The only thing that happens when allocating memory for this type is setting fields to null. Let’s see why this happens: 1. When one declares a variable in the body of a method, the time of memory allocation for a structure is close to zero. That is because the time of allocation for local variables doesn’t depend on their number; 2. If ValTypes are allocated as fields, Reftypes will increase the size of the fields. A Value type is allocated entirely, becoming its part; 3. As in case of copying, if ValTypes are passed as method parameters, there appears a difference, depending on the size and location of a parameter.


However, that doesn’t take more time than copying one variable into another.


The choice between a class or a structure


Let’s discuss the advantages and disadvantages of both types and decide on their use scenarios. A classic principle says we should choose a value type if it is not larger than 16 bytes, stays unchanged during its life and is not inherited. However, choosing the right type means reviewing it from different perspectives basing on scenarios of future use. I propose three groups of criteria:


  • based on type system architecture, in which your type will interact;
  • based on your approach as a system programmer to choosing a type with optimal performance;
  • when there is no other choice.

Each designed feature should reflect its purpose. This doesn’t deal with its name or interaction interface (methods, properties) only. One can use architectural considerations to choose between value and reference types. Let’s think why a structure and not a class might be chosen from the type system architecture's point of view.


  1. If your designed type is agnostic to its state, this will mean its state reflects a process or is a value of something. In other words, an instance of a type is constant and unchangeable by nature. We can create another instance of a type based on this constant by indicating some offset. Or, we can create a new instance by indicating its properties. However, we mustn’t change it. I don’t mean that structure is an immutable type. You can change its field values. Moreover, you can pass a reference to a structure into a method using the ref parameter and you will get changed fields after exiting the method. What I talk here about is architectural sense. I will give several examples.


    • DateTime is a structure which encapsulates the concept of a moment in time. It stores this data as a uint but gives access to separate characteristics of a moment in time: year, month, day, hour, minutes, seconds, milliseconds and even processor ticks. However, it is unchangeable, basing on what it encapsulates. We cannot change a moment in time. I cannot live the next minute as if it was my best birthday in the childhood. Thus, if we choose a data type, we can choose a class with a readonly interface, which produces a new instance for each change of properties. Or, we can choose a structure, which can but shouldn’t change the fields of its instances: its value is the description of a moment in time, like a number. You cannot access the structure of a number and change it. If you want to get another moment in time, which differs for one day from original, you will just get a new instance of a structure.
    • KeyValuePair<TKey, TValue> is a structure that encapsulates the concept of a connected key–value pair. This structure is only to output the content of a dictionary during enumeration. From the architectural point of view a key and a value are inseparable concepts in Dictionary<T>. However, inside we have a complex structure, where a key lies separately from a value. For a user a key-value pair is an inseparable concept in terms of interface and the meaning of a data structure. It is an entire value itself. If one assigns another value for a key, the whole pair will change. Thus, they represent a single entity. This makes a structure an ideal variant in this case.

  2. If your designed type is an inseparable part of an external type but is integral structurally. That means it is incorrect to say the external type refers to an instance of an encapsulated type. However, it is correct to say that an encapsulated type is a part of an external together with all its properties. This is useful when designing a structure which is a part of another structure.


    • For example, if we take a structure of a file header it will be inappropriate to pass a reference from one file to another, e.g. some header.txt file. This would be appropriate when inserting a document in another, not by embedding a file but using a reference in a file system. A good example is shortcut files in Windows OS. However, if we talk about a file header (for example JPEG file header containing metadata about an image size, compression methods, photography parameters, GPS coordinates and other), then we should use structures to design types for parsing the header. If you describe all the headers in structures, you will get the same position of fields in memory as it is in a file. Using simple unsafe *(Header *)readedBuffer transformation without deserialization you will get fully filled data structures.


  1. Neither example shows the inheritance of behavior. They show that there is no need to inherit the behavior of these entities. They are self-contained. However, if we take the effectiveness of code into consideration, we will see the choice from another side:
  2. If we need to take some structured data from unmanaged code, we should choose structures. We can also pass data structure to an unsafe method. A reference type is not suitable for this at all.
  3. A structure is your choice if a type passes the data in method calls (as returned values or as a method parameter) and there is no need to refer to the same value from different places. The perfect example is tuples. If a method returns several values using tuples, it will return a ValueTuple, declared as a structure. The method won’t allocate space on the heap, but will use the stack of the thread, where memory allocation costs nothing.
  4. If you design a system that creates big traffic of instances that have small size and lifetime, using reference types will lead either to a pool of objects or, if without the pool of objects, to an uncontrolled garbage accumulation on the heap. Some objects will turn into older generations, increasing the load on GC. Using value types in such places (if it’s possible) will give an increase in performance because nothing will pass to the SOH. This will lessen the load on GC and the algorithm will work faster;

Basing on what I’ve said, here is some advice on using structures:


  1. When choosing collections you should avoid big arrays storing big structures. This includes data structures based on arrays. This can lead to a transition to the Large Objects Heap and its fragmentation. It is wrong to think that if our structure has 4 fields of the byte type, it will take 4 bytes. We should understand that in 32-bit systems each structure field is aligned on 4 bytes boundaries (each address field should be divided exactly by 4) and in 64-bit systems — on 8 bytes boundaries. The size of an array should depend on the size of a structure and a platform, running a program. In our example with 4 bytes – 85K / (from 4 to 8 bytes per field * the number of fields = 4) minus the size of an array header equals to about 2 600 elements per array depending on the platform (this should be rounded down). That is not very much. It may have seemed that we could easily reach a magic constant of 20 000 elements
  2. Sometimes you use a big size structure as a source of data and place it as a field in a class, while having one copy replicated to produce a thousand of instances. Then you expand each instance of a class for the size of a structure. It will lead to the swelling of generation zero and transition to generation one and even two. If the instances of a class have a short life period and you think the GC will collect them at generation zero – for 1 ms, you will be disappointed. They are already in generation one and even two. This makes the difference. If the GC collects generation zero for 1 ms, the generations one and two are collected very slowly that will lead to a decrease in efficiency;
  3. For the same reason you should avoid passing big structures through a series of method calls. If all elements call each other, these calls will take more space on the stack and bring your application to death by StackOverflowException. The next reason is performance. The more copies there are the more slowly everything works.

That’s why the choice of a data type is not an obvious process. Often, this can refer to a premature optimization, which is not recommended. However, if you know your situation falls within above stated principles, you can easily choose a value type.


This chapter was translated from Russian jointly by author and by professional translators. You can help us with translation from Russian or English into any other language, primarily into Chinese or German.

Also, if you want thank us, the best way you can do that is to give us a star on github or to fork repository github/sidristij/dotnetbook.
Tags:
Hubs:
+31
Comments 1
Comments Comments 1

Articles

Information

Website
clrium.ru
Registered
Founded
Employees
1 employee (me only)
Location
Россия
Representative
Stanislav Sidristij