C # a un mot clé ref. En utilisant ref, vous pouvez passer un int à une méthode par référence. Que se passe-t-il dans le cadre de la pile lorsque vous appelez une méthode qui accepte un int par référence?

public void SampleMethod(ref int i) { }
20
Foo 2 janv. 2016 à 21:16

4 réponses

Meilleure réponse

Passer une variable locale comme référence

Au niveau bas, la variable locale int référencée sera placée sur la pile (la plupart du temps, les entiers sont stockés dans les registres), et un pointeur vers la pile sera passé à la fonction invoquée (le pointeur lui-même est le plus susceptible d'être passé dans un registre). Prenons l'exemple suivant:

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

Ce sera JIT-et à quelque chose comme ça (l'architecture cible est 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  

Passer un élément de tableau ou un membre d'objet comme référence

À peu près la même chose se produit ici, l'adresse du champ ou de l'élément est obtenue et le pointeur est passé à la fonction:

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

Compile vers (sans la partie ennuyeuse):

    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]  

Notez que le tableau ne doit pas être épinglé dans ce cas , car le le framework sait que l'adresse dans ecx pointe un élément à l'intérieur du tableau, donc si une compression de tas se produit entre lea et call ou à l'intérieur de la fonction inc, il peut réajuster directement la valeur de ecx.

Vous pouvez étudier l'assembly JIT-ed vous-même à l'aide du débogueur Visual Studio en ouvrant la fenêtre de désassemblage ( Debug / Windows / Disassembly )

18
Tamas Hegedus 26 juin 2019 à 09:26

L'adresse de la variable locale ou du champ. Dans l'IL, L'instruction ldloca.s est utilisée pour une variable locale.

Charge l'adresse de la variable locale à un index spécifique sur la pile d'évaluation

L'instruction stind est utilisée pour stocker la valeur dans la variable

Stocker la valeur de type (...) en mémoire à l'adresse

L'adresse est 32/64 bits, selon l'architecture cible.

5
Jakub Lortz 2 janv. 2016 à 18:54

Voici un exemple simple en code C #:

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

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

Voici le code IL généré

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     

Notez avec ce cas simple il n'y a vraiment qu'une seule différence ldloca.s 00 ou ldloc.0. Charger l'adresse locale ou charger (de l'offset 00)

C'est la différence au niveau le plus simple (ce que vous avez demandé dans votre commentaire) - si vous chargez la valeur de la variable ou si vous chargez l'adresse de la variable. Les choses peuvent se compliquer rapidement - si la fonction que vous appelez n'est pas locale, si la variable que vous passez n'est pas locale, etc. Mais au niveau de base, c'est la différence.

J'ai utilisé linqpad pour faire mon montage rapide - je le recommande. http://www.linqpad.net/

4
Hogan 2 janv. 2016 à 18:59

Il passera la variable locale par référence au lieu d'envoyer une nouvelle copie pour elle

0
yazan_ati 2 janv. 2016 à 21:54