【问题标题】:Does passing Reference Types using ref save memory?使用 ref 传递引用类型会节省内存吗?
【发布时间】:2011-11-02 13:33:22
【问题描述】:

在 C# 中,方法的参数可以是引用类型或值类型。传递引用类型时,传递引用的副本。这样,如果在方法内部我们尝试将传递的引用重新分配给另一个对象实例,则在方法外部重新分配是不可见的。

为了实现这一点,C# 具有 ref 修饰符。使用 ref 传递引用类型实际上使用原始引用而不是副本。 (如果我错了,请纠正我)。

在这种情况下,由于我们没有创建引用的副本,我们是否节省了任何内存?如果一个方法被广泛调用,这是否会提高应用程序的整体性能?

谢谢!

【问题讨论】:

  • 我认为你实际上不应该受到表现的激励(至少在这种情况下不是)。就像 Mehrdad 在他的回答中所说的那样,只有在需要从方法中更改引用时才应该使用 ref。如果您的方法必须设置它的值,请考虑使用“out”关键字,而不是“ref”。
  • 在任何一种情况下,复制引用都不是您的瓶颈。
  • @Kornelije Petak 你的意思是我不应该假设正在发生任何内部指针算术,而是将 ref 修饰符视为允许修改引用的机制?
  • @史密斯先生:没错。 ref 指针的大小与 Object 引用的大小相同,因此您在此处获得 nothing —— 只是让您的应用程序变慢(非常少)。
  • @Misters 节省的钱本来就在堆栈上。顺序调用会重用内存,递归调用会在你的第 100 万次调用之前得到一个 StackOverFlow。

标签: c# .net pass-by-reference ref reference-type


【解决方案1】:

声明

不,它没有。如果有的话,它会因为额外的查找而变慢。

没有理由通过引用传递引用类型,除非您以后特别打算分配给它。


证明

由于有些人似乎认为编译器传递的是“变量本身”,所以看看这段代码的反汇编:

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);
    }
}

这是(在 x86 上,为简单起见):

// 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

这是来自代码的优化编译。显然,传递的是变量的地址,而不是“变量本身”。

【讨论】:

  • 我认为不涉及额外的查找。
  • @Daniel:在两种情况复制了一个指针。只是它是直接指针还是指向另一个指针的指针才是问题所在 - 当您使用 ref 时,是后者,这涉及额外的内存查找(除非 JIT 通过证明它不必要来优化它)。跨度>
  • @Mehrdad:不正确。引用类型只是底层的指针。当不通过 ref 时,您将创建一个新指针。我的回答证明了这一点。也就是说,不通过 ref 将需要 sizeof(pointer) (通常在 32 位系统上为 32 位)比通过 ref 的版本更多的内存。
  • @Mehrdad:当通过 ref 传递时,您不会传递指向指针的指针,而是将唯一的指针传递给您的对象,而不是在不通过 ref 传递时创建新的指针。跨度>
  • @Mister:区别似乎正是我已经解释过的:第二个 copy 对象引用(它是按值传递的),而第一个传递一个 pointer 指向 Object 引用(它是通过引用传递的,它是通过指针传递的)。所以第一种情况需要被调用者两个指针查找,而第二种情况只需要一个(因为它已经有原始指针的副本,并且不需要查找它)。
【解决方案2】:

Mehrdad 示例的分解视图(两个版本)

对于像我这样不擅长阅读汇编代码的人,我将尝试更深入地挖掘 Mehrdad 的好证明。当我们调试时,可以在 Visual Studio 中捕获此代码,单击 Debug -> Windows -> Dissasembly。

使用 REF 的版本

源代码:

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

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

汇编语言 (x86)(仅显示不同的部分):

             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              
         }  

没有参考的版本

源代码:

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

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

汇编语言 (x86)(仅显示不同的部分):

             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              
         }

除了注释行之外,两个版本的代码是相同的:使用 ref,对函数的调用前面有一条 LEA 指令,没有 ref,我们有一个更简单的 MOV 指令。执行此行后,LEA 已将指向对象指针的指针加载到 ecx 寄存器中,而 MOV 已将指向对象的指针加载到 ecx 寄存器中。这意味着 FD30B000 子例程(指向我们的测试函数)在第一种情况下必须额外访问内存才能访问对象。如果我们检查这个函数的每个生成版本的汇编代码,我们可以看到在某些时候(实际上是两个版本之间唯一不同的行)进行了额外的访问:

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

而没有 ref 的函数可以直接进入对象:

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

希望对您有所帮助。

【讨论】:

    【解决方案3】:

    是的,有一个原因:如果您想重新分配值。在这方面,值类型和引用类型没有区别。

    看下面的例子:

    class A
    {
        public int B {get;set;}
    }
    
    void ReassignA(A a)
    {
      Console.WriteLine(a.B);
      a = new A {B = 2};
      Console.WriteLine(a.B);
    }
    
    // ...
    A a = new A { B = 1 };
    ReassignA(a);
    Console.WriteLine(a.B);
    

    这将输出:

    1
    2
    1
    

    然而,性能与它无关。这将是真正的微优化。

    【讨论】:

    • @downvoter:请发表评论。这个答案应该是正确的 - 但是,如果没有评论,我无法改进它。
    【解决方案4】:

    按值传递引用类型不会复制对象。它只创建对现有对象的新引用。所以你不应该通过引用传递它,除非你真的需要。

    【讨论】:

      猜你喜欢
      • 2010-11-25
      • 1970-01-01
      • 2019-04-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多