使用 ref 而不是返回相同类型的性能成本?

发布于 12-15 12:07 字数 828 浏览 4 评论 0原文

嗨,这确实困扰着我,我希望有人能为我解答。我一直在阅读有关 ref (和 out)的内容,并且我试图弄清楚我是否使用 ref 减慢了我的代码速度s。通常我会替换类似:

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

with

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

因为在我看来,这

AddToInt(ref _value, _add);

比这更容易阅读和编写代码,

_value = AddToInt(_value, _add);

我确切地知道我使用 ref 在代码上做了什么,而不是返回一个值。然而,性能是我非常重视的事情,并且当您使用 refs 时,显然取消引用和清理会慢很多。

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

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

Hi this is something that's really bothering me and I'm hoping someone has an answer for me. I've been reading about ref (and out) and I'm trying to figure out if I'm slowing down my code using refs. Commonly I will replace something like:

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

with

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

because to my eyes this

AddToInt(ref _value, _add);

is easier to read AND code than this

_value = AddToInt(_value, _add);

I know precisely what I'm doing on the code using ref, as opposed to returning a value. However, performance is something I take seriously, and apparently dereferencing and cleanup is a lot slower when you use refs.

What I'd like to know is why every post I read says there is very few places you would typically pass a ref (I know the examples are contrived, but I hope you get the idea), when it seems to me that the ref example is smaller, cleaner and more exact.

I'd also love to know why ref really is slower than returning a value type - to me it would seem to me, if I was going to edit the function value a lot before returning it, that it would be quicker to reference the actual variable to edit it as opposed to an instance of that variable shortly before it gets cleaned from memory.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

不语却知心2024-12-22 12:07:55

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

  • 防止在堆栈上多次复制过大的结构
  • 防止由于改变结构副本而导致数据丢失(XNA 结构通常是可变的,与正常做法相反)
  • 允许直接,而不是将其复制出来并返回

在所有其他情况下,“ref”更常见地与额外的副作用相关联,不容易在返回值中表达(例如,参见Monitor.TryEnter)。

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

最后,返回方式更加通用;您并不总是想更新源。对比:

// 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”紧随其后),并且在不明确的语言(例如 VB)中,ref 的使用甚至不太清晰

The main time that "ref" is used in the same sentence as performance is when discussing some very atypical cases, for example in XNA scenarios where the game "objects" are quite commonly represented by structs rather than classes to avoid problems with GC (which has a disproportionate impact on XNA). This becomes useful to:

  • prevent copying an oversized struct multiple times on the stack
  • prevent data loss due to mutating a struct copy (XNA structs are commonly mutable, against normal practice)
  • allow passing a struct in an an array directly, rather than ever copying it out and back in

In all other cases, "ref" is more commonly associated with an additional side-effect, not easily expressed in the return value (for example see Monitor.TryEnter).

If you don't have a scenario like the XNA/struct one, and there is no awkward side effect, then just use the return value. In addition to being more typical (which in itself has value), it could well involve passing less data (and int is smaller than a ref on x64 for example), and could require less dereferencing.

Finally, the return approach is more versatile; you don't always want to update the source. Contrast:

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

I think the last is the least clear (with the other "ref" one close behind it) and ref usage is even less clear in languages where it is not explicit (VB for example).

随波逐流2024-12-22 12:07:55

使用 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 更新的变量传递到函数中时,只需 1 次写入操作即可获得结果。然而,当返回值时,您通常会写入函数内的某个变量,返回它,然后再次将其写入目标变量。根据数据的不同,这可能会增加不必要的开销。无论如何,这些是我在使用 ref 关键字之前通常考虑的主要事情。

有时,在 C# 中这样使用 ref 会更快一些,但不足以将其用作性能的 goto 理由。

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

输出:

迭代:10000000
参考:165ms
拜瓦尔:417毫秒

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

The main purpose of using the ref keyword is to signify that the variable's value can be changed by the function its being passed into. When you pass a variable by value, updates from within the function don't effect the original copy.

Its extremely useful (and faster) for situations when you want multiple return values and building a special struct or class for the return values would be overkill. For example,

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

This is a pretty fundamental pattern in languages that have unrestricted use of pointers. In c/c++ you frequently see primitives being passed around by value with classes and arrays as pointers. C# does just the opposite so 'ref' is handy in situations like the above.

When you pass a variable you want updated into a function by ref, only 1 write operation is necessary to give you your result. When returning values however, you normally write to some variable inside the function, return it, then write it again to the destination variable. Depending on the data, this could add unnecessary overhead. Anyhow, these are the main things that I typically consider before using the ref keyword.

Sometimes ref is a little faster when used like this in c# but not enough to use it as a goto justification for performance.

Here's what I got on a 7 year old machine using the code below passing and updating a 100k string by ref and by value.

Output:

iterations: 10000000
byref: 165ms
byval: 417ms

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;
}
假装不在乎2024-12-22 12:07:55

我必须同意 Ondrej 的观点。从风格的角度来看,如果你开始用 ref 传递所有内容,你最终会与开发人员合作,他们会因为你设计这样的 API 而想掐死你!

只需从方法中返回内容,不要让 100% 的方法返回 void。你所做的将会导致非常不干净的代码,并且可能会让最终处理你的代码的其他开发人员感到困惑。在这里优先考虑清晰度而不是性能,因为无论如何您都不会在优化中获得太多收益。

查看此帖子:C# 'ref' 关键字,性能

以及 Jon Skeet 的这篇文章: http://www.yoda.arachsys.com/csharp/parameters.html

I have to agree with Ondrej here. From a stylistic view, if you start passing everything with ref you will eventually end up working with devs who will want to strangle you for designing an API like this!

Just return stuff from the method, don't have 100% of your methods returning void. What you are doing will lead to very unclean code and might confuse other devs who end up working on your code. Favour clarity over performance here, since you won't gain much in optomization anyway.

check this SO post: C# 'ref' keyword, performance

and this article from Jon Skeet: http://www.yoda.arachsys.com/csharp/parameters.html

一个人练习一个人2024-12-22 12:07:55

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

如果对象较大,例如包含大量字符的字符串,则通过 ref 传递非常有用。

Using ref for basic datatypes is not a good idea.
Especially for simple methods which have few lines of code.
First off C# compiler will do lots of optimizations to make the code faster.
As per my benchmark https://rextester.com/CQJR12339 passing by ref has degraded the performance.
When a reference is passed , you are copying 8bytes as a pointer variable (assuming 64 bit processor) why not pass a 8Bytes double directly?

Pass by ref is useful in case of larger objects , For example a string which has lots of characters.

羞稚2024-12-22 12:07:55

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

通过引用传递只能用于中型到大型数据结构,例如 3D 模型或数组。

in your case, using ref is a bad idea.
when using ref, the program will read a pointer off of the stack, than read the value the pointer is pointing at.
but if you were to pass by value it would only need to read the value off of the stack, basically reducing the amount of reads by half.

passing by reference should only be used for medium to large data structures like, for example, 3D models or arrays.

写给空气的情书2024-12-22 12:07:55

首先,不要担心使用 ref 是更慢还是更快。这是过早的优化。在 99.9999% 的情况下,您不会遇到导致性能瓶颈的情况。

其次,由于类 C 语言通常具有“函数式”性质,因此首选将计算结果作为返回值返回,而不是使用 ref。它可以更好地链接语句/调用。

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

.NET 6.0.7 的结果:

方法平均误差StdDev
AddToIntBase375.6 us5.36 us4.18 us
AddToIntReturn378.6 us7.22 us7.41 us
AddToIntReturnInline375.5 us4.84 us4.04 us
AddToIntRef383.7 us5.92 us5.25 us
AddToIntRefInline384.2 us7.29 us8.96 us

.NET 4.8的结果(因为这是一个11年前的问题)本质上是相同的:

MethodMeanErrorStdDev
AddToIntBase381.3 us7.40 us7.92 us
AddToIntReturn380.8 美元7.00 美元6.55 us
AddToIntReturnInline380.0 us5.03 us4.20 us
AddToIntRef378.5 us6.62 us5.87 us
AddToIntRefInline381.5 us4.50 us3.76 us

基准代码:

public class AddToIntBenchmark
{
    [Benchmark]
    public void AddToIntBase()
    {
        int result = 0;
        for (int i = 0; i < 1_000_000; i++) result += i;
    }

    [Benchmark]
    public void AddToIntReturn()
    {
        int result = 0;
        for (int i = 0; i < 1_000_000; i++) result = AddToInt(result, i);
    }

    [Benchmark]
    public void AddToIntReturnInline()
    {
        int result = 0;
        for (int i = 0; i < 1_000_000; i++) result = AddToIntInline(result, i);
    }

    [Benchmark]
    public void AddToIntRef()
    {
        int result = 0;
        for (int i = 0; i < 1_000_000; i++) AddToInt(ref result, i);
    }

    [Benchmark]
    public void AddToIntRefInline()
    {
        int result = 0;
        for (int i = 0; i < 1_000_000; i++) AddToIntInline(ref result, i);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private int AddToIntInline(int original, int add) { return original + add; }

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

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void AddToIntInline(ref int original, int add) { original += add; }

    private void AddToInt(ref int original, int add) { original += add; }
}

环境:.NET 6.0.7、Intel Xeon Gold 16 核 2.4 GHz、 WS2019虚拟机

First, don't bother whether using ref is slower or faster. It's premature optimization. In 99.9999% cases you won't run into a situation this would cause a performance bottleneck.

Second, returning the result of the calculation as a return value as opposed to using ref is preferred because of the usual 'functional' nature of C-like languages. It leads to better chaining of statements/calls.

Update: Adding evidence from an actual performance benchmark, which shows the difference is ~1%, and hence readability over premature optimization is the suggested approach. Plus, ref turns out to be actually slower than a return value; however, as the difference is so small, repeated runs of the benchmark may end up opposite.

Results for .NET 6.0.7:

MethodMeanErrorStdDev
AddToIntBase375.6 us5.36 us4.18 us
AddToIntReturn378.6 us7.22 us7.41 us
AddToIntReturnInline375.5 us4.84 us4.04 us
AddToIntRef383.7 us5.92 us5.25 us
AddToIntRefInline384.2 us7.29 us8.96 us

Results for .NET 4.8 (because this is an 11-year-old question) are essentially the same:

MethodMeanErrorStdDev
AddToIntBase381.3 us7.40 us7.92 us
AddToIntReturn380.8 us7.00 us6.55 us
AddToIntReturnInline380.0 us5.03 us4.20 us
AddToIntRef378.5 us6.62 us5.87 us
AddToIntRefInline381.5 us4.50 us3.76 us

Benchmark code:

public class AddToIntBenchmark
{
    [Benchmark]
    public void AddToIntBase()
    {
        int result = 0;
        for (int i = 0; i < 1_000_000; i++) result += i;
    }

    [Benchmark]
    public void AddToIntReturn()
    {
        int result = 0;
        for (int i = 0; i < 1_000_000; i++) result = AddToInt(result, i);
    }

    [Benchmark]
    public void AddToIntReturnInline()
    {
        int result = 0;
        for (int i = 0; i < 1_000_000; i++) result = AddToIntInline(result, i);
    }

    [Benchmark]
    public void AddToIntRef()
    {
        int result = 0;
        for (int i = 0; i < 1_000_000; i++) AddToInt(ref result, i);
    }

    [Benchmark]
    public void AddToIntRefInline()
    {
        int result = 0;
        for (int i = 0; i < 1_000_000; i++) AddToIntInline(ref result, i);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private int AddToIntInline(int original, int add) { return original + add; }

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

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void AddToIntInline(ref int original, int add) { original += add; }

    private void AddToInt(ref int original, int add) { original += add; }
}

Environment: .NET 6.0.7, Intel Xeon Gold 16-core 2.4 GHz, WS2019 virtual machine

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文