Search code examples
.netgarbage-collectionclr

.NET CLR Smallest Memory Allocation (32/64-bit)


I'm trying to get to the bottom of the lowest block of memory that can be allocated via the CLR in both 32-bit and 64-bit systems. It seems to me that on a 32-bit system, it will allocate in 4 byte chunks and on 64-bit it will allocate in 8 byte chunks. If true, does an Int32 need 8 bytes of address space on 64-bit system?


Solution

  • If you check the following code:

    public class Numbers
    {
        public Int16 A = 1;
        public Int32 B = 2;
        public Int64 C = 3;
        public UInt16 D = 4;
        public UInt32 E = 5;
        public UInt64 F = 6;
        public short G = 7;
        public int H = 8;
    }
    

    Compiling and running that using x64 as a target platform yields the following instructions when viewed in the Disassembly view:

    7:         public Int16 A = 1;
    mov         rcx,qword ptr [rbp+50h]  
    mov         word ptr [rcx+24h],1  
    
     8:         public Int32 B = 2;
    mov         rcx,qword ptr [rbp+50h]  
    mov         dword ptr [rcx+18h],2  
    
     9:         public Int64 C = 3;
    mov         ecx,3  
    movsxd      rcx,ecx  
    mov         rax,qword ptr [rbp+50h]  
    mov         qword ptr [rax+8],rcx  
    
    10:         public UInt16 D = 4;
    mov         rcx,qword ptr [rbp+50h]  
    mov         word ptr [rcx+26h],4  
    
    11:         public UInt32 E = 5;
    mov         rcx,qword ptr [rbp+50h]  
    mov         dword ptr [rcx+1Ch],5  
    
    12:         public UInt64 F = 6;
    mov         ecx,6  
    movsxd      rcx,ecx  
    mov         rax,qword ptr [rbp+50h]  
    mov         qword ptr [rax+10h],rcx  
    
    13:         public short G = 7;
    mov         rcx,qword ptr [rbp+50h]  
    mov         word ptr [rcx+28h],7  
    
    14:         public int H = 8;
    mov         rcx,qword ptr [rbp+50h]  
    mov         dword ptr [rcx+20h],8  
    mov         rcx,qword ptr [rbp+50h]  
    

    From this you can see that it allocates 2, 4, and 8 bytes depending on the datatype. It isn't so that it allocates in chunks of the wordsize of the processor, on Windows a word is always 16 bits, a double word is always 32 bits, and on x64 you have quadruple words.

    To prove it you can view the address space in memory and how the values are stacked up:

    ![Memory block allocations

    Now you must note that since all memory as managed by the CLR, this is not always a predictable behavior. As you see from my example, it allocated 8 bytes for an Int64 and a UInt64, 4 bytes for an Int32, UInt32 and an int, and only 2 bytes for a UInt16 and a short.

    If we were to use even smaller numbers, such as byte or bool like this:

    public class Numbers
    {
        public byte I = 10;
        public bool J = true;
    }
    

    We get another result:

     7:         public byte I = 10;
    mov         rcx,qword ptr [rbp+50h]  
    mov         byte ptr [rcx+8],0Ah  
    
     8:         public bool J = true;
    mov         rcx,qword ptr [rbp+50h]  
    mov         byte ptr [rcx+9],1  
    

    Now it uses bytes, and uses only 1 byte per.

    Allocation of 2 bytes

    The object lives on the heap in a chunk of 64 bits: Object viewed on the heap

    However it isn't always so that the CLR will organize this in memory this way. As you can see from the heap map, the CLR allocated a chunk of 64 bytes for the object, and tries to fit all the object pieces inside there. It does so using as much cleverness as possible.

    I tried several different versions of this, and the allocation space depends on behavior, and if the CLR allocates too little, it will reallocate somewhere else and move the pointer, for instance the first 8 bytes of the address space is used to hold the 3, it then has some of the object related data in the first 4 bytes, thus sharing the 8 bytes with other data. And it will continue to do so until it needs that space to hold the value for 'C'. One way it does that is to see if it can shift C up to gain the occupied space.

    So to answer your question the smallest chunk it will allocate will vary on the application and how the CLR chooses to organize memory for your program. You shouldn't think of your types as chunks of memory, think that their size represent the maximum needed memory to hold the max/min values of them.

    A good rule of thumb is to think that it allocates the maximum required space the type calls for, rounded to bytes, but since the CLR will manage all this for you, you do not really need to worry about it. Memory considerations on .NET should be focused on object creation and life-cycle management, not value types.