Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does passing Reference Types using ref save memory?

In C#, the parameters to a method can be either reference types or value types. When passing reference types, a copy of the reference is passed. This way, if inside a method we try to reassign the passed reference to another object instance, outside of the method the reassignment is not visible.

To make this working, C# has the ref modifier. Passing a reference type with ref actually uses the original reference instead of a copy. (Correct me if I'm wrong).

In this case, since we are not creating a copy of the reference, are we saving any memory? If a method is extensively called, does this improve the overall performance of the application?

Thanks!

like image 570
Mister Smith Avatar asked Aug 24 '11 07:08

Mister Smith


2 Answers

Claim

No, it doesn't. If anything, it's slower because of the extra lookup.

There's no reason to pass a reference type by reference unless you specifically intend to assign to it later.


Proof

Since some people seem to think that the compiler passes "the variable itself", take a look at the disassembly of this code:

using System;

static class Program
{
    static void Test(ref object o) { GC.KeepAlive(o); }

    static void Main(string[] args)
    {
        object temp = args;
        Test(ref temp);
    }
}

which is (on x86, for simplicity):

// Main():
// Set up the stack
00000000  push        ebp                    // Save the base pointer
00000001  mov         ebp,esp                // Set up stack pointer
00000003  sub         esp,8                  // Reserve space for local variables
00000006  xor         eax,eax                // Zero out the EAX register

// Copy the object reference to the local variable `temp` (I /think/)
00000008  mov         dword ptr [ebp-4],eax  // Copy its content to memory (temp)
0000000b  mov         dword ptr [ebp-8],ecx  // Copy ECX (where'd it come from??)
0000000e  cmp         dword ptr ds:[00318D5Ch],0  // Compare this against zero
00000015  je          0000001C               // Jump if it was null (?)
00000017  call        6F910029               // (Calls some internal method, idk)

// THIS is where our code finally starts running
0000001c  mov         eax,dword ptr [ebp-8]  // Copy the reference to register
0000001f  mov         dword ptr [ebp-4],eax  // ** COPY it AGAIN to memory
00000022  lea         ecx,[ebp-4]            // ** Take the ADDRESS of the copy
00000025  call        dword ptr ds:[00319734h] // Call the method

// We're done with the call
0000002b  nop                                // Do nothing (breakpoint helper)
0000002c  mov         esp,ebp                // Restore stack
0000002e  pop         ebp                    // Epilogue
0000002f  ret                                // Return

This was from an optimized compilation of the code. Clearly, there's an address of a variable being passed, and not "the variable itself".

like image 177
user541686 Avatar answered Sep 24 '22 17:09

user541686


DISSASEMBLER VIEW OF Mehrdad's example (BOTH VERSIONS)

I'll try to dig a little deeper on Mehrdad's nice proof, for those like me that are not very good reading assembly code. This code can be captured in Visual Studio when we're debbuging, clicking Debug -> Windows -> Dissasembly.

VERSION USING REF

Source Code:

 namespace RefTest
 {
    class Program
    {
        static void Test(ref object o) { GC.KeepAlive(o); }

        static void Main(string[] args)
        {
            object temp = args;
            Test(ref temp);
        }
    }
 }

Assembly language (x86) (only showing the part that differs):

             object temp = args;
 00000030  mov         eax,dword ptr [ebp-3Ch] 
 00000033  mov         dword ptr [ebp-40h],eax 
             Test(ref temp);
 00000036  lea         ecx,[ebp-40h] //loads temp address's address on ecx? 
 00000039  call        FD30B000      
 0000003e  nop              
         }  

VERSION WITHOUT REF

Source Code:

 namespace RefTest
 {
    class Program
    {
        static void Test(object o) { GC.KeepAlive(o); }

        static void Main(string[] args)
        {
            object temp = args;
            Test(temp);
        }
    }
 }

Assembly language (x86) (only showing the part that differs):

             object temp = args;
 00000035  mov         eax,dword ptr [ebp-3Ch] 
 00000038  mov         dword ptr [ebp-40h],eax 
             Test(temp);
 0000003b  mov         ecx,dword ptr [ebp-40h] //move temp address to ecx?
 0000003e  call        FD30B000 
 00000043  nop              
         }

Apart from the commented line, the code is the same for both versions: with ref, the call to the function is preceded by a LEA instruction, without ref we've a simpler MOV instruction. After executing this line, LEA has loaded the ecx register with a pointer to a pointer to the object, whereas MOV has loaded ecx with a pointer to the object. This means that the FD30B000 subroutine (pointing to our Test function) in the first case will have to make an extra access to memory to get to the object. If we inspect the assembly code for each produced version of this function, we can see that at some point (in fact the only line that differs between the two versions) the extra access is made:

static void Test(ref object o) { GC.KeepAlive(o); }
...
00000025  mov         eax,dword ptr [ebp-3Ch] 
00000028  mov         ecx,dword ptr [eax]
...

While the function without ref can go straight to the object:

static void Test(object o) { GC.KeepAlive(o); }
...
00000025  mov         ecx,dword ptr [ebp-3Ch]
...

Hope it helped.

like image 27
Mister Smith Avatar answered Sep 25 '22 17:09

Mister Smith