🌀

C#

CategoryC#

.NET

Common Language Runtime

The Common Language Runtime (CLR) is the .NET runtime.

Base Class Library

The Base Class Library (BCL) is the core library of .NET.

Garbage Collector

The Garbage Collector (GC) is a process that periodically determines what part of the physical memory is in use, and collects unused data.

The behavior of the GC is non deterministic.

The GC is mark & sweep and generational.

Mark & sweep

The GC marks objects to collect them once they are dereferenced.

Generational

The GC divides objects into generations according to their age.

Objects are collected one generation at a time.

The GC collects objects in generation 0.

Memory

Managed Memory

The managed memory of the process is the memory managed by the .NET runtime and cleaned by the GC.

The .NET runtime is responsible for allocating and deallocating the memory.

It's not possible to control the managed memory manually.

Managed memory is allocated with the new operator, followed by the type to allocate and initialize.

⚠Allocating and deallocating reference types over time will increase the work for the GC and create memory fragmentation.

Stack Memory

The stack memory is not garbage collected.

During the execution of a method, the stackalloc operator allocates a block of memory on the stack. The memory is deallocated automatically when the method returns.

The stackalloc operator is used like the new operator.

When the allocated memory is assigned to a pointer, the operator must be used in an unsafe context.

unsafe
{
  int* numbers = stackalloc int[3];
  ...
}

When the allocated memory is assigned to a Span<T>, the operator don't need to be used in an unsafe context.

Span<int> numbers = stackalloc int[3];
...

Unmanaged Memory

The unmanaged memory (or native memory) of the process is the memory that is not managed by the .NET runtime.

Unmanaged memory in .NET is handled through:

Interop

Interop (or P/Invoke) allows communication with native libraries or the Windows API.

The System.Runtime.InteropServices namespace provide the functionality to perform interop.

A function can be defined with the DLLImport attribute to allow the corresponding native function from the specified DLL to be called from managed code and .NET will handle marshalling.

[DllImport("kernel32.dll")]
static extern void OutputDebugString(string lpOutputString);

Marshalling

The process of transitioning between unmanaged memory and managed objects is called marshalling.

The Marshal class provides the functionality to perform marshalling.

Unmanaged memory can be allocated by calling the Marshal.AllocHGlobal method. The memory requires the number of bytes to allocate and returns a IntPtr to the block of memory.

public static IntPtr AllocHGlobal(int cb);

The memory must be released using the Marshal.FreeHGlobal method with the IntPtr that was returned during allocation.

public static void FreeHGlobal(IntPtr hglobal);

For example, we allocate 100 bytes.

IntPtr hglobal = Marshal.AllocHGlobal(100);
Marshal.FreeHGlobal(hglobal);

ℹThe memory is allocated on the heap using GlobalAlloc or LocalAlloc depending on the implementation.

Unsafe code

Methods and code areas can be defined with the unsafe keyword allowing pointer operations and object pinning. The memory won't be moved by the GC.

The IntPtr type is a struct that represents a platform-specific pointer.

The type is a 32-bits integer on 32-bit systems, and a 64-bits integer on 64-bit systems.

IntPtr values can be converted to actual pointers with a cast.

The pointer can then dereferenced and assigned a value with the new keyword.

A value type can be allocated on the heap using this method.

struct MyStruct
{
	public int Value;
}
 
unsafe void MyMethod()
{
	var ptr = (MyStruct*)Marshal.AllocHGlobal(sizeof(MyStruct));
  *ptr = new MyStruct { Value = 100 };

  Marshal.FreeHGlobal((IntPtr)ptr);
}

Layout

By default, the members are laid out sequentially if they don't contain any pointers.

The physical layout of the fields can be controlled with the StructLayout attribute and the specified LayoutKind value.

The LayoutKind.Auto value lets the CLR choose an appropriate layout for the members.

If the type contains pointers, LayoutKind.Auto is the default layout.

Alignment

The default alignment of a type is the size of its largest field.

Fields are aligned by their size. For example:

⚠Embedded structs are aligned on word boundaries even if they are smaller.

The StructLayout attribute has an additional Pack value that controls the alignment of the members.

By default, the value is 0, indicating packing on word boundaries.

Setting the value to 1 packs the members tightly but can decrease the performances because of unaligned memory operations.

Types

Value types

Value types are declared with the struct keyword.

The .NET primitive types are also value types (bool, int, float, ...).

Struct

Reference types

References types (or objects) are declared with the class keyword.

Class

Base types

String

A string is a sequence of Unicode characters.

A string is represented by the System.String type. string is an alias for String.

A character is represented by the System.Char type. char is an alias for Char.

Characters are represented by UTF-16 code units.

SIMD

SIMD instructions are supported using the NuGet package Microsoft.Bcl.Simd.

Boxing

Boxing is the process of converting a value type to a reference type (such as an interface type). When the CLR boxes a value type, it wraps the value inside a Object instance and stores it on the managed heap.

Boxing is an implicitly conversion.

Unboxing extracts the value type from the object reference.

Unboxing is an explicit conversion that requires a cast to a value type.

⚠The GC tracks references to boxed value types.

ℹBoxing should be avoid as it allocates memory and adds work for the GC.

ℹBoxing can be avoided using generic constraints.


When a value type needs to behave like an object, boxing happens.

Pinning

Pinning is used to prevent the GC from moving an object in memory.

During the Compact Phase, the GC moves object in memory to reduce fragmentation.

An object is pinned to allow unmanaged code to access it using a pointer, and during pinning its memory address must not change.

There two methods to perform pinning.

ℹP/Invoke automaticlaly pins object for the duration of the method call.

Static Constructor

A static constructor is used to initialize static data or to perform an action only.

ℹDoes not accept arguments.

ℹDoes not accept access modifiers.

ℹCalled automatically when the type is loaded in the application domain (before the first instance is created or any static members are referenced).

ℹExceptions that are throw in static constructors are wrapped inside a TypeInitializationException.

⚠The order of execution of static constructors depends on the CLR.

⚠Types with static constructors do not benefit from the beforefieldinit flag optimization (lazy initialization of the type).

✔Useful to implement a singleton.

Destructor

A destructor is called a finalizer.

When an object has a finalizer, it is stored in a finalization queue when it is allocated.

When the GC collects the object, it puts it in the f-reachable queue.

A specialized CLR thread monitors the f-reachable queue, and calls the finalizer methods to give a chance to the objects to perform some cleanup (typically unmanaged references).

⚠Adding a finalizer method to a class has a high performance cost as the GC is not able to collect to object immediatly.

⚠The finalizer method is always called from a CLR thread.

Unsafe type

A struct or a class can be declared with the unsafe keyword, to allow all its code to be used in an unsafe context.

unsafe class MyClass
{
    static void Copy(byte* source, byte* destination, int count) {...}
}

Otherwise, the unsafe keyword needs to be specified in a member declaration.

class MyClass
{
    unsafe static void Copy(byte* source, byte* destination, int count) {...}
}

Fixed-size arrays

When an array is defined as a field in a type, it is stored as a pointer.

The array can be initialized to any size and the reference will point to the corresponding array data.

An array can defined as a fixed-size array with the fixed keyword and a size value.

The type that contains the fixed-size array must be defined as unsafe.

unsafe struct MyStruct
{
    public fixed int buffer[1024];
}

Readonly struct

A readonly struct can only contain readonly fields that are immutable.

readonly struct MyStruct
{
    public readonly int MyValue = 42;
}

Patterns

Lazy Allocation

private MyInstance _instance;

public MyInstance Instance
{
    get
    {
        if (_instance == null)
        {
            _instance = new MyInstance();
        }
        return _instance;
    }
}
✔️
The cost of allocating the property is deferred to its access
The property might be called from multiple threads. You need to use locks to protect the if statement.
private MyInstance _instance;

public MyInstance Instance
{
    get
    {
        lock (this)
        {
            if (_instance == null)
            {
                _instance = new MyInstance();
            }
        }
        return _instance;
    }
}
⚠️
Acquiring the lock takes a nontrivial amount of time.