C# 中的 in 参数和性能分析
<p><code>in</code> 修饰符也是从 C# 7.2 开始引入的,它与我们上一篇中讨论的 《C# 中的只读结构体(readonly struct)》<sup class="footnote-ref"></sup> 是紧密相关的。</p><h2 id="in-修饰符">in 修饰符</h2>
<p><code>in</code> 修饰符通过引用传递参数。 它让形参成为实参的别名,即对形参执行的任何操作都是对实参执行的。 它类似于 <code>ref</code> 或 <code>out</code> 关键字,不同之处在于 <code>in</code> 参数无法通过调用的方法进行修改。</p>
<ul>
<li><code>ref</code> 修饰符,指定参数由引用传递,可以由调用方法读取或写入。</li>
<li><code>out</code> 修饰符,指定参数由引用传递,必须由调用方法写入。</li>
<li><code>in</code> 修饰符,指定参数由引用传递,可以由调用方法读取,但不可以写入。</li>
</ul>
<p>举个简单的例子:</p>
<pre><code class="language-csharp">struct Product
{
public int ProductId { get; set; }
public string ProductName { get; set; }
}
public static void Modify(in Product product)
{
//product = new Product(); // 错误 CS8331 无法分配到 变量 'in Product',因为它是只读变量
//product.ProductName = "测试商品";// 错误 CS8332 不能分配到 变量 'in Product' 的成员,因为它是只读变量
Console.WriteLine($"Id: {product.ProductId}, Name: {product.ProductName}"); // OK
}
</code></pre>
<h2 id="引入-in-参数的原因">引入 in 参数的原因</h2>
<p>我们知道,结构体实例的内存在栈(stack)上进行分配,所占用的内存随声明它的类型或方法一起回收,所以通常在内存分配上它是比引用类型占有优势的。<sup class="footnote-ref"></sup></p>
<p>但是对于有些很大(比如有很多字段或属性)的结构体,将其作为方法参数,在紧凑的循环或关键代码路径中调用方法时,复制这些结构的成本就会很高。当所调用的方法不修改该参数的状态,使用新的修饰符 <code>in</code> 声明参数以指定此参数可以按引用安全传递,可以避免(可能产生的)高昂的复制成本,从而提高代码运行的性能。</p>
<h2 id="in-参数对性能的提升">in 参数对性能的提升</h2>
<p>为了测试 <code>in</code> 修饰符对性能的提升,我定义了两个较大的结构体,一个是可变的结构体 <code>NormalStruct</code>,一个是只读的结构体 <code>ReadOnlyStruct</code>,都定义了 30 个属性,然后定义三个测试方法:</p>
<ul>
<li><code>DoNormalLoop</code> 方法,参数不加修饰符,传入一般结构体,这是以前比较常见的做法。</li>
<li><code>DoNormalLoopByIn</code> 方法,参数加 <code>in</code> 修饰符,传入一般结构体。</li>
<li><code>DoReadOnlyLoopByIn</code> 方法,参数加 <code>in</code> 修饰符,传入只读结构体。</li>
</ul>
<p>代码如下所示:</p>
<pre><code class="language-csharp">public struct NormalStruct
{
public decimal Number1 { get; set; }
public decimal Number2 { get; set; }
//...
public decimal Number30 { get; set; }
}
public readonly struct ReadOnlyStruct
{
// 自动属性上的 readonly 关键字是可以省略的,这里加上是为了便于理解
public readonly decimal Number1 { get; }
public readonly decimal Number2 { get; }
//...
public readonly decimal Number30 { get; }
}
public class BenchmarkClass
{
const int loops = 50000000;
NormalStruct normalInstance = new NormalStruct();
ReadOnlyStruct readOnlyInstance = new ReadOnlyStruct();
public decimal DoNormalLoop()
{
decimal result = 0M;
for (int i = 0; i < loops; i++)
{
result = Compute(normalInstance);
}
return result;
}
public decimal DoNormalLoopByIn()
{
decimal result = 0M;
for (int i = 0; i < loops; i++)
{
result = ComputeIn(in normalInstance);
}
return result;
}
public decimal DoReadOnlyLoopByIn()
{
decimal result = 0M;
for (int i = 0; i < loops; i++)
{
result = ComputeIn(in readOnlyInstance);
}
return result;
}
public decimal Compute(NormalStruct s)
{
//业务逻辑...
return 0M;
}
public decimal ComputeIn(in NormalStruct s)
{
//业务逻辑...
return 0M;
}
public decimal ComputeIn(in ReadOnlyStruct s)
{
//业务逻辑...
return 0M;
}
}
</code></pre>
<p>在没有使用 <code>in</code> 参数的方法中,意味着每次调用传入的是变量的一个新副本; 而在使用 <code>in</code> 修饰符的方法中,每次不是传递变量的新副本,而是传递同一副本的只读引用。</p>
<p>使用 BenchmarkDotNet 工具测试三个方法的运行时间,结果如下:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th style="text-align: right">Mean</th>
<th style="text-align: right">Error</th>
<th style="text-align: right">StdDev</th>
<th style="text-align: right">Median</th>
<th style="text-align: right">Ratio</th>
<th style="text-align: right">RatioSD</th>
</tr>
</thead>
<tbody>
<tr>
<td>DoNormalLoop</td>
<td style="text-align: right">1,536.3 ms</td>
<td style="text-align: right">65.07 ms</td>
<td style="text-align: right">191.86 ms</td>
<td style="text-align: right">1,425.7 ms</td>
<td style="text-align: right">1.00</td>
<td style="text-align: right">0.00</td>
</tr>
<tr>
<td>DoNormalLoopByIn</td>
<td style="text-align: right">480.9 ms</td>
<td style="text-align: right">27.05 ms</td>
<td style="text-align: right">79.32 ms</td>
<td style="text-align: right">446.3 ms</td>
<td style="text-align: right">0.32</td>
<td style="text-align: right">0.07</td>
</tr>
<tr>
<td>DoReadOnlyLoopByIn</td>
<td style="text-align: right">581.9 ms</td>
<td style="text-align: right">35.71 ms</td>
<td style="text-align: right">105.30 ms</td>
<td style="text-align: right">594.1 ms</td>
<td style="text-align: right">0.39</td>
<td style="text-align: right">0.10</td>
</tr>
</tbody>
</table>
<p>从这个结果可以看出,如果使用 <code>in</code> 参数,不管是一般的结构体还是只读结构体,相对于不用 <code>in</code> 修饰符的参数,性能都有较大的提升。这个性能差异在不同的机器上运行可能会有所不同,但是毫无疑问,使用 <code>in</code> 参数会得到更好的性能。</p>
<h3 id="在-parallelfor-中使用">在 Parallel.For 中使用</h3>
<p>在上面简单的 <code>for</code> 循环中,我们看到 <code>in</code> 参数有助于性能的提升,那么在并行运算中呢?我们把上面的 <code>for</code> 循环改成使用 <code>Parallel.For</code> 来实现,代码如下:</p>
<pre><code class="language-csharp">
public decimal DoNormalLoop()
{
decimal result = 0M;
Parallel.For(0, loops, i => Compute(normalInstance));
return result;
}
public decimal DoNormalLoopByIn()
{
decimal result = 0M;
Parallel.For(0, loops, i => ComputeIn(in normalInstance));
return result;
}
public decimal DoReadOnlyLoopByIn()
{
decimal result = 0M;
Parallel.For(0, loops, i => ComputeIn(in readOnlyInstance));
return result;
}
</code></pre>
<p>事实上,道理是一样的,在没有使用 <code>in</code> 参数的方法中,每次调用传入的是变量的一个新副本; 在使用 <code>in</code> 修饰符的方法中,每次传递的是同一副本的只读引用。</p>
<p>使用 BenchmarkDotNet 工具测试三个方法的运行时间,结果如下:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th style="text-align: right">Mean</th>
<th style="text-align: right">Error</th>
<th style="text-align: right">StdDev</th>
<th style="text-align: right">Ratio</th>
</tr>
</thead>
<tbody>
<tr>
<td>DoNormalLoop</td>
<td style="text-align: right">793.4 ms</td>
<td style="text-align: right">13.02 ms</td>
<td style="text-align: right">11.54 ms</td>
<td style="text-align: right">1.00</td>
</tr>
<tr>
<td>DoNormalLoopByIn</td>
<td style="text-align: right">352.4 ms</td>
<td style="text-align: right">6.99 ms</td>
<td style="text-align: right">17.27 ms</td>
<td style="text-align: right">0.42</td>
</tr>
<tr>
<td>DoReadOnlyLoopByIn</td>
<td style="text-align: right">341.1 ms</td>
<td style="text-align: right">6.69 ms</td>
<td style="text-align: right">10.02 ms</td>
<td style="text-align: right">0.43</td>
</tr>
</tbody>
</table>
<p>同样表明,使用 <code>in</code> 参数会得到更好的性能。</p>
<h2 id="使用-in-参数需要注意的地方">使用 in 参数需要注意的地方</h2>
<p>我们来看一个例子,定义一个一般的结构体,包含一个属性 <code>Value</code> 和 一个修改该属性的方法 <code>UpdateValue</code>。 然后在别的地方也定义一个方法 <code>UpdateMyNormalStruct</code> 来修改该结构体的属性 <code>Value</code>。<br>
代码如下:</p>
<pre><code class="language-csharp">struct MyNormalStruct
{
public int Value { get; set; }
public void UpdateValue(int value)
{
Value = value;
}
}
class Program
{
static void UpdateMyNormalStruct(MyNormalStruct myStruct)
{
myStruct.UpdateValue(8);
}
static void Main(string[] args)
{
MyNormalStruct myStruct = new MyNormalStruct();
myStruct.UpdateValue(2);
UpdateMyNormalStruct(myStruct);
Console.WriteLine(myStruct.Value);
}
}
</code></pre>
<p>您可以猜想一下它的运行结果是什么呢? 2 还是 8?</p>
<p>我们来理一下,在 <code>Main</code> 中先调用了结构体自身的方法 <code>UpdateValue</code> 将 <code>Value</code> 修改为 2, 再调用 <code>Program</code> 中的方法 <code>UpdateMyNormalStruct</code>, 而该方法中又调用了 <code>MyNormalStruct</code> 结构体自身的方法 <code>UpdateValue</code>,那么输出是不是应该是 8 呢? 如果您这么想就错了。<br>
它的正确输出结果是 <strong>2</strong>,这是为什么呢?</p>
<p>这是因为,结构体和许多内置的简单类型(sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool 和 enum 类型)一样,都是值类型,在传递参数的时候以值的方式传递。因此调用方法 <code>UpdateMyNormalStruct</code> 时传递的是 <code>myStruct</code> 变量的新副本,在此方法中,其实是此副本调用了 <code>UpdateValue</code> 方法,所以原变量 <code>myStruct</code> 的 <code>Value</code> 不会发生变化。</p>
<p>说到这里,有聪明的朋友可能会想,我们给 <code>UpdateMyNormalStruct</code> 方法的参数加上 <code>in</code> 修饰符,是不是输出结果就变为 8 了,<code>in</code> 参数不就是引用传递吗?<br>
我们可以试一下,把代码改成:</p>
<pre><code class="language-csharp">static void UpdateMyNormalStruct(in MyNormalStruct myStruct)
{
myStruct.UpdateValue(8);
}
static void Main(string[] args)
{
MyNormalStruct myStruct = new MyNormalStruct();
myStruct.UpdateValue(2);
UpdateMyNormalStruct(in myStruct);
Console.WriteLine(myStruct.Value);
}
</code></pre>
<p>运行一下,您会发现,结果依然为 <strong>2</strong> !这……就让人大跌眼镜了……<br>
用工具查看一下 <code>UpdateMyNormalStruct</code> 方法的中间语言:</p>
<pre><code class="language-csharp">.method private hidebysig static
void UpdateMyNormalStruct (
valuetype ConsoleApp4InTest.MyNormalStruct& myStruct
) cil managed
{
.param
.custom instance void System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
01 00 00 00
)
// Method begins at RVA 0x2164
// Code size 18 (0x12)
.maxstack 2
.locals init (
valuetype ConsoleApp4InTest.MyNormalStruct
)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldobj ConsoleApp4InTest.MyNormalStruct
IL_0007: stloc.0
IL_0008: ldloca.s 0
IL_000a: ldc.i4.8
IL_000b: call instance void ConsoleApp4InTest.MyNormalStruct::UpdateValue(int32)
IL_0010: nop
IL_0011: ret
} // end of method Program::UpdateMyNormalStruct
</code></pre>
<p>您会发现,在 <code>IL_0002</code>、<code>IL_0007</code> 和 <code>IL_0008</code> 这几行,仍然创建了一个 <code>MyNormalStruct</code> 结构体的防御性副本(<code>defensive copy</code>)。虽然在调用方法 <code>UpdateMyNormalStruct</code> 时以引用的方式传递参数,但在方法体中调用结构体自身的 <code>UpdateValue</code> 前,却创建了一个该结构体的防御性副本,改变的是该副本的 <code>Value</code>。这就有点奇怪了,不是吗?</p>
<p>我们使用 <code>in</code> 参数的目的就是想减少结构体的复制从而提升性能,但这里并没有起到作用。甚至,假如 <code>UpdateMyNormalStruct</code> 方法中多次调用该结构体的<em>非只读方法</em>,编译器也会多次创建该结构体的防御性副本,这就对性能产生了负面影响。</p>
<p>Google 了一些资料是这么解释的:C# 无法知道当它调用一个结构体上的方法(或getter)时,是否也会修改它的值/状态。于是,它所做的就是创建所谓的“防御性副本”。当在结构体上运行方法(或getter)时,它会创建传入的结构体的副本,并在副本上运行方法。这意味着原始副本与传入时完全相同,调用者传入的值并没有被修改。</p>
<p>有没有办法让方法 <code>UpdateMyNormalStruct</code> 调用后输出 8 呢?您将参数改成 <code>ref</code> 修饰符试试看 😜 😁 😂</p>
<p>综上所述,<strong>最好不要把 <code>in</code> 修饰符和一般<em>(非只读)</em>结构体一起使用,以免产生晦涩难懂的行为,而且可能对性能产生负面影响。</strong></p>
<h2 id="in-参数的限制">in 参数的限制</h2>
<p>不能将 <code>in</code>、<code>ref</code> 和 <code>out</code> 关键字用于以下几种方法:</p>
<ul>
<li>异步方法,通过使用 <code>async</code> 修饰符定义。</li>
<li>迭代器方法,包括 <code>yield return</code> 或 <code>yield break</code> 语句。</li>
<li>扩展方法的第一个参数不能有 <code>in</code> 修饰符,除非该参数是结构体。</li>
<li>扩展方法的第一个参数,其中该参数是泛型类型(即使该类型被约束为结构体。)</li>
</ul>
<h2 id="总结">总结</h2>
<ul>
<li>使用 <code>in</code> 参数,有助于明确表明此参数不可修改的意图。</li>
<li>当<strong>只读结构体(<code>readonly struct</code>)</strong>的大小大于 <code>IntPtr.Size</code> <sup class="footnote-ref"></sup> 时,出于性能原因,应将其作为 <code>in</code> 参数传递。</li>
<li>不要将一般<em>(非只读)</em>结构体作为 <code>in</code> 参数,因为结构体是可变的,反而有可能对性能产生负面影响,并且可能产生晦涩难懂的行为。</li>
</ul>
<br>
<blockquote>
<p>作者 : 技术译民<br>
出品 : 技术译站</p>
</blockquote>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>https://www.cnblogs.com/ittranslator/p/13876180.html C# 中的只读结构体 ↩︎</p>
</li>
<li id="fn2" class="footnote-item"><p>https://www.cnblogs.com/ittranslator/p/13664383.html C# 中 Struct 和 Class 的区别总结 ↩︎</p>
</li>
<li id="fn3" class="footnote-item"><p>https://docs.microsoft.com/zh-cn/dotnet/api/system.intptr.size#System_IntPtr_SizeIntPtr.Size ↩︎</p>
</li>
</ol>
</section>
</div>
<div id="MySignature" role="contentinfo">
<div><p style="font-size: 14px; font-family: '微软雅黑';font-weight: 400; padding: 0 0 5px 2px;color:#888;">© 转载请标明出处 https://www.cnblogs.com/ittranslator</p></div>
<div style="text-align: center;max-width: 280px;margin: 10px auto;">
<p style="font-size: 18px; font-weight: 600; color: rgba(0, 0, 0, 1); padding-top: 6px; padding-bottom: 6px; border-bottom: 1px dashed rgba(119, 119, 255, 1)">不做标题党,只分享技术干货
</p><p style="font-size: 13px; font-weight: 400; padding-top: 6px; padding-bottom: 0px;color:rgb(66,66,166);">公众号『技术译站』,<b>欢迎扫码关注</b></p>
<img style="width: 215px;" src="https://img2020.cnblogs.com/blog/2074831/202006/2074831-20200628152541133-1651846078.jpg" alt="">
</div><br><br>
来源:https://www.cnblogs.com/ittranslator/p/13919691.html
頁:
[1]