Как работает ключевое слово ref (с точки зрения памяти)

В С# есть ключевое слово ref. Используя ref, вы можете передать int методу по ссылке. Что происходит с фреймом стека при вызове метода, который принимает int по ссылке?

public void SampleMethod(ref int i) { }

Ответы

Ответ 1

Передача локальной переменной в качестве ссылки

На низком уровне ссылочная локальная переменная int будет помещена в стек (большую часть времени целые числа хранятся в регистрах), а указатель на стек будет передан вызываемой функции (сам указатель, скорее всего, будет передан в реестре). Рассмотрим следующий пример:

var i = 7;
Console.WriteLine(i);
inc(ref i);
Console.WriteLine(i);

Это будет JIT-et к чему-то вроде этого (целевая архитектура - x86):

    17:             var i = 7;
    # allocate space on the stack for args and i
00482E3B  sub         esp,8  
    # initialize i to 0
00482E3E  xor         eax,eax  
00482E40  mov         dword ptr [ebp-8],eax  
    # args saved to stack (could be optimised out)  
00482E43  mov         dword ptr [ebp-4],ecx  
00482E46  cmp         dword ptr ds:[3ACAECh],0  
00482E4D  je          00482E54  
00482E4F  call        7399CB2D  
    # i = 7
00482E54  mov         dword ptr [ebp-8],7  
    18:             Console.WriteLine(i);
    # load the value of i into ecx, and call cw
00482E5B  mov         ecx,dword ptr [ebp-8]  
00482E5E  call        72E729DC  
    19:             inc(ref i);
    # load the address of i into ecx, and call inc
00482E63  lea         ecx,[ebp-8]  
00482E66  call        dword ptr ds:[4920860h]  
    20:             Console.WriteLine(i);
    # load the value of i into ecx, and call cw
00482E6C  mov         ecx,dword ptr [ebp-8]  
00482E6F  call        72E729DC  
    21:         }
00482E74  nop  
00482E75  mov         esp,ebp  
00482E77  pop         ebp  
00482E78  ret  

Передача элемента массива или члена объекта в качестве ссылки

Здесь происходит почти то же самое, получается адрес поля или элемента и указатель передается функции:

var i = new[]{7};
Console.WriteLine(i[0]);
inc(ref i[0]);
Console.WriteLine(i[0]);

Компилируется в (без скучной части):

    18:             Console.WriteLine(i[0]);
00C82E91  mov         eax,dword ptr [ebp-8]  
00C82E94  cmp         dword ptr [eax+4],0  
00C82E98  ja          00C82E9F  
00C82E9A  call        7399BDC2  
00C82E9F  mov         ecx,dword ptr [eax+8]  
00C82EA2  call        72E729DC  
    19:             inc(ref i[0]);
    # loading the reference of the array to eax
00C82EA7  mov         eax,dword ptr [ebp-8]  
    # array boundary check is inlined
00C82EAA  cmp         dword ptr [eax+4],0  
00C82EAE  ja          00C82EB5  
    # this would throw an OutOfBoundsException, but skipped by ja
00C82EB0  call        7399BDC2  
    # load the address of the element in ecx, and call inc
00C82EB5  lea         ecx,[eax+8]  
00C82EB8  call        dword ptr ds:[4F80860h]  

Обратите внимание, что в этом случае массив не должен быть закреплен, потому что фреймворк знает, что адрес в ecx указывает на элемент внутри массива, поэтому, если сжатие кучи происходит между lea и call или внутри функции inc, он может перенастройте значение ecx напрямую.

Вы можете исследовать JIT-сборку самостоятельно, используя отладчик Visual Studio, открыв окно Разборка (Отладка /Windows/Разборка)

Ответ 2

Адрес локальной переменной или поля. В IL для локальной переменной используется ldloca.s.

Загружает адрес локальной переменной по определенному индексу в стек оценки

Команда stind используется для сохранения значения обратно в переменной

Сохранить значение типа (...) в памяти по адресу

Адрес 32/64 бит, в зависимости от целевой архитектуры.

Ответ 3

Вот простой пример в коде С#:

void Main()
{
    int i = 1;
    inc(ref i);
    Console.WriteLine(i);
}

public void inc(ref int i) { 
  i++;
}

Вот сгенерированный код IL

IL_0000:  nop         
IL_0001:  ldc.i4.1    
IL_0002:  stloc.0     // i
IL_0003:  ldarg.0     
IL_0004:  ldloca.s    00 // i
IL_0006:  call        inc
IL_000B:  nop         
IL_000C:  ldloc.0     // i
IL_000D:  call        System.Console.WriteLine
IL_0012:  nop         
IL_0013:  ret         

inc:
IL_0000:  nop         
IL_0001:  ldarg.1     
IL_0002:  dup         
IL_0003:  ldind.i4    
IL_0004:  ldc.i4.1    
IL_0005:  add         
IL_0006:  stind.i4    
IL_0007:  ret     

Обратите внимание, что в этом простом случае действительно существует только одна разница ldloca.s 00 или ldloc.0. Загрузка локального или загрузочного адреса (смещения 00)

Это разница на самом простом уровне (это то, о чем вы просили в своем комментарии) - если вы загружаете значение переменной или загружаете адрес переменной. Вещь может быстро усложниться - если вызываемая вами функция не является локальной, если переменная, которую вы передаете, не является локальной и т.д. И т.д. Но на базовом уровне это разница.

Я использовал linqpad для выполнения моей быстрой диссонализации - я рекомендую его. http://www.linqpad.net/

Ответ 4

Он будет передавать локальную переменную по ссылке вместо отправки новой копии для нее