C#
Category | C# |
---|
.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
- Unsafe code
- Marshalling
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.
- Allocating and deallocating unmanaged memory
- Copying unmanaged memory blocks
- Converting managed to unmanaged types
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:
- Byte fields align on 1-byte boundaries
- Int16 fields align on 2-byte boundaries
- Int32 fields align on 4-byte boundaries
⚠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
, ...).
- Allocated on the stack
- Allocated on the heap when part of a reference type
- Not managed by the GC
- Passed and returned by value by default (value copy)
- The
Equals
method performs a bitwise comparison when possible (no reference fields); otherwise the comparison is performed with reflection.
Struct
- Structs implicitly derive from
System.ValueType
- Structs are implicitly sealed
- A
ref struct
is always stored on the stack. It cannot be part of a class or a regular struct.
Reference types
References types (or objects) are declared with the class
keyword.
- Allocated on the heap
- Managed by the GC
- Passed and returned by reference (pointer copy)
- The
Equals
method performs a reference comparison
Class
- Classes implicitly derive from
System.Object
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.
- String is a reference type.
- Strings are immutable.
- String overrides the
==
operator to perform a string comparison.
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.
- A value type is assigned to an interface it implements.
- A value type is passed as a parameter of a method as an
Object
reference.
- A member from the
Object
type is called from a value type suchGetHashCode
orEquals
.
- ℹWhen a member from the
Object
type is overriden in the value type, boxing is avoided.
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.
- The
fixed
keyword: used for a short duration in a code block.
- The
GCHandle
class: longer lifetime (can cause GC overhead).
ℹ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;
}
}
private MyInstance _instance;
public MyInstance Instance
{
get
{
lock (this)
{
if (_instance == null)
{
_instance = new MyInstance();
}
}
return _instance;
}
}