使用 ref 而不是返回相同类型的性能成本?
嗨,这确实困扰着我,我希望有人能为我解答。我一直在阅读有关 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 ref
s. 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 技术交流群。

发布评论
评论(6)
使用 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;
}
我必须同意 Ondrej 的观点。从风格的角度来看,如果你开始用 ref
传递所有内容,你最终会与开发人员合作,他们会因为你设计这样的 API 而想掐死你!
只需从方法中返回内容,不要让 100% 的方法返回 void
。你所做的将会导致非常不干净的代码,并且可能会让最终处理你的代码的其他开发人员感到困惑。在这里优先考虑清晰度而不是性能,因为无论如何您都不会在优化中获得太多收益。
查看此帖子:C# 'ref' 关键字,性能
以及 Jon Skeet 的这篇文章: http://www.yoda.arachsys.com/csharp/parameters.html
对基本数据类型使用 ref 并不是一个好主意。
特别是对于只有几行代码的简单方法。
首先,C# 编译器会进行大量优化以使代码更快。
根据我的基准测试 https://rextester.com/CQJR12339 通过 ref 会降低性能。
当传递引用时,您正在复制 8 个字节作为指针变量(假设是 64 位处理器)为什么不直接传递 8 个字节的双精度?
如果对象较大,例如包含大量字符的字符串,则通过 ref 传递非常有用。
首先,不要担心使用 ref
是更慢还是更快。这是过早的优化。在 99.9999% 的情况下,您不会遇到导致性能瓶颈的情况。
其次,由于类 C 语言通常具有“函数式”性质,因此首选将计算结果作为返回值返回,而不是使用 ref。它可以更好地链接语句/调用。
更新:添加来自实际性能基准的证据,显示差异约为 1%,因此建议的方法是过早优化的可读性。另外,ref
实际上比返回值慢;然而,由于差异很小,重复运行基准测试可能会得到相反的结果。
.NET 6.0.7 的结果:
方法 | 平均 | 误差 | StdDev |
---|---|---|---|
AddToIntBase | 375.6 us | 5.36 us | 4.18 us |
AddToIntReturn | 378.6 us | 7.22 us | 7.41 us |
AddToIntReturnInline | 375.5 us | 4.84 us | 4.04 us |
AddToIntRef | 383.7 us | 5.92 us | 5.25 us |
AddToIntRefInline | 384.2 us | 7.29 us | 8.96 us |
.NET 4.8的结果(因为这是一个11年前的问题)本质上是相同的:
Method | Mean | Error | StdDev |
---|---|---|---|
AddToIntBase | 381.3 us | 7.40 us | 7.92 us |
AddToIntReturn | 380.8 美元 | 7.00 美元 | 6.55 us |
AddToIntReturnInline | 380.0 us | 5.03 us | 4.20 us |
AddToIntRef | 378.5 us | 6.62 us | 5.87 us |
AddToIntRefInline | 381.5 us | 4.50 us | 3.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虚拟机
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
“ref”与性能在同一个句子中使用的主要时间是在讨论一些非常非典型的情况时,例如在 XNA 场景中,游戏“对象”通常由结构而不是类表示,以避免 GC 问题(这对 XNA 产生不成比例的影响)。这对于以下方面非常有用:
在所有其他情况下,“ref”更常见地与额外的副作用相关联,不容易在返回值中表达(例如,参见
Monitor.TryEnter
)。如果您没有像 XNA/struct 这样的场景,并且没有尴尬的副作用,那么只需使用返回值即可。除了更典型(其本身有价值)之外,它很可能涉及传递更少的数据(例如,int 小于 x64 上的引用),并且可能需要更少的取消引用。
最后,返回方式更加通用;您并不总是想更新源。对比:
我认为最后一个是最不清晰的(另一个“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:
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:
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).