In this article we will create a class, aka a reference type on the stack, and therefore don't use any managed memory!
Here is the "simple" code:
public record MyClass(int Number);
var typeHandleValue = typeof(MyClass).TypeHandle.Value;
var size = Marshal.ReadInt32(typeHandleValue, 4);
var mem = stackalloc byte[size];
*(IntPtr*)mem = typeHandleValue;
var instance = Unsafe.AsRef<MyClass>(&mem);
The whole idea is to create a pointer to the type handle of the class and then use the pointer to create an instance of the class on the stack.
We are using stackalloc
to allocate the memory on the stack and then use Unsafe.AsRef
to create a reference to the allocated memory.
Is it useful? Probably not, but it is possible! And that is all I wanted to show.
The "main issues" with this approach:
- You are not calling any constructor
- Your fields are all "uninitialized" - in the sense that integers are not necessarily 0
- The size has to be static. You can't increase or decrease the size.
So, all in all: Yes, this is a class coated as a struct! With all its downside and almost no upsides.
Here is a small benchmark:
[MemoryDiagnoser]
public class Benchmarks
{
private static readonly IntPtr TypeHandleValue = typeof(MyClass).TypeHandle.Value;
private static readonly int Size = Marshal.ReadInt32(TypeHandleValue, 4);
[Benchmark]
public unsafe MyClass OnStack()
{
var mem = stackalloc byte[Size];
*(IntPtr*)mem = TypeHandleValue;
return Unsafe.AsRef<MyClass>(mem);
}
[Benchmark]
public MyClass OnHeap() => new MyClass(0);
}
public record MyClass(int Number);
With the following result:
| Method | Mean | Error | StdDev | Gen0 | Allocated |
|-------- |----------:|----------:|----------:|-------:|----------:|
| OnStack | 0.6014 ns | 0.0136 ns | 0.0127 ns | - | - |
| OnHeap | 1.1356 ns | 0.0270 ns | 0.0252 ns | 0.0029 | 24 B |
As we can see, OnStack
has a better runtime and doesn't allocate any memory. And Number
will also be initialized to something - not necessarily 0. But hey, I never mentioned that the executed program is correct!