使用ref代替返回相同类型的性能代价

本文关键字:同类型 性能 代价 返回 ref 使用 | 更新日期: 2023-09-27 18:14:16

嗨,这是一件真的困扰我的事情,我希望有人能给我一个答案。我一直在阅读ref(和out),我试图弄清楚我是否在使用ref s减慢我的代码。通常我会替换为:

int AddToInt(int original, int add){ return original+add; }

void AddToInt(ref int original, int add){ original+=add; } // 1st parameter gets the result

因为在我眼里这

AddToInt(ref _value, _add);

更容易读AND代码
_value = AddToInt(_value, _add);

我确切地知道我使用ref在代码上做什么,而不是返回一个值。然而,性能是我很重视的,显然当你使用refs时,解引用和清理要慢得多。

我想知道的是为什么我读到的每一篇文章都说很少有地方你通常会通过ref(我知道这些例子是人为的,但我希望你能明白),在我看来,ref的例子更小,更干净,更准确。

我也很想知道为什么ref真的比返回值类型要慢-对我来说,如果我要在返回它之前编辑函数值很多,那么引用实际变量来编辑它会更快,而不是在它从内存中清理之前不久的该变量的实例。

使用ref代替返回相同类型的性能代价

"ref"与性能在同一个句子中使用的主要情况是在讨论一些非常非典型的情况时,例如在XNA场景中,游戏"对象"通常由结构体而不是类来表示,以避免GC问题(这对XNA有不成比例的影响)。这对于以下情况很有用:

  • 防止在堆栈上多次复制超大结构
  • 防止由于结构体副本的变化而导致的数据丢失(XNA结构体通常是可变的,与正常做法相反)
  • 允许直接在数组中传递结构体,而不是将其复制到
  • 中。

在所有其他情况下,"ref"通常与一个额外的副作用联系在一起,不容易在返回值中表达(例如参见Monitor.TryEnter)。

如果您没有像XNA/struct那样的场景,并且没有尴尬的副作用,那么只需使用返回值。除了更典型(它本身就有价值)之外,它可能涉及传递更少的数据(例如,int比x64上的ref小),并且可能需要更少的解引用。

最后,返回方法更通用;您并不总是想要更新源代码。对比:

// want to accumulate, no ref
x = Add(x, 5);
// want to accumulate, ref
Add(ref x, 5);
// no accumulate, no ref
y = Add(x, 5);
// no accumulate, ref
y = x;
Add(ref y, x);

我认为最后一个是最不清楚的(另一个"ref"紧随其后),ref的用法是甚至在不显式的语言中更不清楚(例如VB)。

使用ref关键字的主要目的是表示变量的值可以被传递给它的函数改变。当你按值传递变量时,函数内部的更新不会影响原始副本。

在需要多个返回值的情况下非常有用(并且更快),并且为返回值构建一个特殊的结构或类将是多余的。例如,

public void Quaternion.GetRollPitchYaw(ref double roll, ref double pitch, ref double yaw){
    roll = something;
    pitch = something;
    yaw = something;
}

在不受限制地使用指针的语言中,这是一个非常基本的模式。在c/c++中,你经常看到原语通过值传递,类和数组作为指针。c#的做法正好相反,因此'ref'在上述情况下很方便。

当你传递一个变量,你想通过ref更新到一个函数时,只需要一个写操作就可以得到你的结果。但是,当返回值时,您通常会写入函数内的某个变量,返回它,然后再次将其写入目标变量。根据数据的不同,这可能会增加不必要的开销。无论如何,这些是我在使用ref关键字之前通常考虑的主要事情。

有时ref在c#中这样使用会快一点,但不足以作为性能的goto证明。

这是我在一台7年前的机器上使用下面的代码通过ref和value传递和更新一个100k的字符串得到的结果。

输出:

迭代:10000000byref: 165毫秒按值传递:417 ms

private void m_btnTest_Click(object sender, EventArgs e) {
    Stopwatch sw = new Stopwatch();
    string s = "";
    string value = new string ('x', 100000);    // 100k string
    int iterations = 10000000;
    //-----------------------------------------------------
    // Update by ref
    //-----------------------------------------------------
    sw.Start();
    for (var n = 0; n < iterations; n++) {
        SetStringValue(ref s, ref value);
    }
    sw.Stop();
    long proc1 = sw.ElapsedMilliseconds;
    sw.Reset();
    //-----------------------------------------------------
    // Update by value
    //-----------------------------------------------------
    sw.Start();
    for (var n = 0; n < iterations; n++) {
        s = SetStringValue(s, value);
    }
    sw.Stop();
    long proc2 = sw.ElapsedMilliseconds;
    //-----------------------------------------------------
    Console.WriteLine("iterations: {0} 'nbyref: {1}ms 'nbyval: {2}ms", iterations, proc1, proc2);
}
public string SetStringValue(string input, string value) {
    input = value;
    return input;
}
public void SetStringValue(ref string input, ref string value) {
    input = value;
}

我同意Ondrej的观点。从风格的角度来看,如果你开始使用ref传递所有内容,你最终将与那些想要扼杀你设计这样一个API的开发人员一起工作!

只是从方法返回东西,不要让你的方法100%返回void。你所做的将导致非常不干净的代码,并可能使其他开发人员最终为你的代码工作。在这里,清晰度比性能更重要,因为无论如何你都不会在优化中获得太多好处。

查看此帖子:c# 'ref'关键字、性能

和这篇来自Jon Skeet的文章:http://www.yoda.arachsys.com/csharp/parameters.html

对基本数据类型使用ref不是一个好主意。特别是对于只有几行代码的简单方法。首先,c#编译器会做很多优化,使代码更快。根据我的基准https://rextester.com/CQJR12339传球裁判降低了性能。当传递引用时,您将8字节复制为指针变量(假设64位处理器),为什么不直接传递8bytes双精度?

Pass by ref对于较大的对象很有用,例如包含很多字符的字符串。

在这种情况下,使用ref是一个坏主意。
当使用ref时,程序将从堆栈中读取一个指针,然后读取该指针所指向的值。
但是如果你是按值传递,它只需要从堆栈中读取值,基本上减少了一半的读取量。

通过引用传递的

应该仅用于中型到大型数据结构,例如3D模型或数组。

首先,不要为是否使用ref更慢或更快而烦恼。这是不成熟的优化。在99.9999%的情况下,您不会遇到这会导致性能瓶颈的情况。

第二,将计算结果作为返回值返回,而不是使用ref,因为类c语言通常具有"函数"性质。它可以更好地链接语句/调用。

更新:添加来自实际性能基准的证据,该基准显示差异约为1%,因此建议采用优于过早优化的可读性方法。另外,ref实际上比返回值慢;然而,由于差异很小,重复运行基准测试可能会得到相反的结果。

.NET 6.0.7:

<表类> 错误 StdDev tbody> <<tr> AddToIntBase td> AddToIntReturn td> AddToIntReturnInline td> AddToIntRef td> AddToIntRefInline td> tbody>