in 修饰符也是从 C# 7.2 开始引入的,它与我们上一篇中讨论的 《C# 中的只读结构体(readonly struct)[1] 是紧密相关的。

in 修饰符

in 修饰符通过引用传递参数。 它让形参成为实参的别名,即对形参执行的任何操作都是对实参执行的。 它类似于 refout 关键字,不同之处在于 in 参数无法通过调用的方法进行修改。

  • ref 修饰符,指定参数由引用传递,可以由调用方法读取或写入。
  • out 修饰符,指定参数由引用传递,必须由调用方法写入。
  • in 修饰符,指定参数由引用传递,可以由调用方法读取,但不可以写入。

举个简单的例子:

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
}

引入 in 参数的原因

我们知道,结构体实例的内存在栈(stack)上进行分配,所占用的内存随声明它的类型或方法一起回收,所以通常在内存分配上它是比引用类型占有优势的。[2]

但是对于有些很大(比如有很多字段或属性)的结构体,将其作为方法参数,在紧凑的循环或关键代码路径中调用方法时,复制这些结构的成本就会很高。当所调用的方法不修改该参数的状态,使用新的修饰符 in 声明参数以指定此参数可以按引用安全传递,可以避免(可能产生的)高昂的复制成本,从而提高代码运行的性能。

in 参数对性能的提升

为了测试 in 修饰符对性能的提升,我定义了两个较大的结构体,一个是可变的结构体 NormalStruct,一个是只读的结构体 ReadOnlyStruct,都定义了 30 个属性,然后定义三个测试方法:

  • DoNormalLoop 方法,参数不加修饰符,传入一般结构体,这是以前比较常见的做法。
  • DoNormalLoopByIn 方法,参数加 in 修饰符,传入一般结构体。
  • DoReadOnlyLoopByIn 方法,参数加 in 修饰符,传入只读结构体。

代码如下所示:

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(); [Benchmark(Baseline = true)]
public decimal DoNormalLoop()
{
decimal result = 0M;
for (int i = 0; i < loops; i++)
{
result = Compute(normalInstance);
}
return result;
} [Benchmark]
public decimal DoNormalLoopByIn()
{
decimal result = 0M;
for (int i = 0; i < loops; i++)
{
result = ComputeIn(in normalInstance);
}
return result;
} [Benchmark]
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;
}
}

在没有使用 in 参数的方法中,意味着每次调用传入的是变量的一个新副本; 而在使用 in 修饰符的方法中,每次不是传递变量的新副本,而是传递同一副本的只读引用。

使用 BenchmarkDotNet 工具测试三个方法的运行时间,结果如下:

Method Mean Error StdDev Median Ratio RatioSD
DoNormalLoop 1,536.3 ms 65.07 ms 191.86 ms 1,425.7 ms 1.00 0.00
DoNormalLoopByIn 480.9 ms 27.05 ms 79.32 ms 446.3 ms 0.32 0.07
DoReadOnlyLoopByIn 581.9 ms 35.71 ms 105.30 ms 594.1 ms 0.39 0.10

从这个结果可以看出,如果使用 in 参数,不管是一般的结构体还是只读结构体,相对于不用 in 修饰符的参数,性能都有较大的提升。这个性能差异在不同的机器上运行可能会有所不同,但是毫无疑问,使用 in 参数会得到更好的性能。

在 Parallel.For 中使用

在上面简单的 for 循环中,我们看到 in 参数有助于性能的提升,那么在并行运算中呢?我们把上面的 for 循环改成使用 Parallel.For 来实现,代码如下:

[Benchmark(Baseline = true)]
public decimal DoNormalLoop()
{
decimal result = 0M;
Parallel.For(0, loops, i => Compute(normalInstance));
return result;
} [Benchmark]
public decimal DoNormalLoopByIn()
{
decimal result = 0M;
Parallel.For(0, loops, i => ComputeIn(in normalInstance));
return result;
} [Benchmark]
public decimal DoReadOnlyLoopByIn()
{
decimal result = 0M;
Parallel.For(0, loops, i => ComputeIn(in readOnlyInstance));
return result;
}

事实上,道理是一样的,在没有使用 in 参数的方法中,每次调用传入的是变量的一个新副本; 在使用 in 修饰符的方法中,每次传递的是同一副本的只读引用。

使用 BenchmarkDotNet 工具测试三个方法的运行时间,结果如下:

Method Mean Error StdDev Ratio
DoNormalLoop 793.4 ms 13.02 ms 11.54 ms 1.00
DoNormalLoopByIn 352.4 ms 6.99 ms 17.27 ms 0.42
DoReadOnlyLoopByIn 341.1 ms 6.69 ms 10.02 ms 0.43

同样表明,使用 in 参数会得到更好的性能。

使用 in 参数需要注意的地方

我们来看一个例子,定义一个一般的结构体,包含一个属性 Value 和 一个修改该属性的方法 UpdateValue。 然后在别的地方也定义一个方法 UpdateMyNormalStruct 来修改该结构体的属性 Value

代码如下:

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

您可以猜想一下它的运行结果是什么呢? 2 还是 8?

我们来理一下,在 Main 中先调用了结构体自身的方法 UpdateValueValue 修改为 2, 再调用 Program 中的方法 UpdateMyNormalStruct, 而该方法中又调用了 MyNormalStruct 结构体自身的方法 UpdateValue,那么输出是不是应该是 8 呢? 如果您这么想就错了。

它的正确输出结果是 2,这是为什么呢?

这是因为,结构体和许多内置的简单类型(sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool 和 enum 类型)一样,都是值类型,在传递参数的时候以值的方式传递。因此调用方法 UpdateMyNormalStruct 时传递的是 myStruct 变量的新副本,在此方法中,其实是此副本调用了 UpdateValue 方法,所以原变量 myStructValue 不会发生变化。

说到这里,有聪明的朋友可能会想,我们给 UpdateMyNormalStruct 方法的参数加上 in 修饰符,是不是输出结果就变为 8 了,in 参数不就是引用传递吗?

我们可以试一下,把代码改成:

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

运行一下,您会发现,结果依然为 2 !这……就让人大跌眼镜了……

用工具查看一下 UpdateMyNormalStruct 方法的中间语言:

.method private hidebysig static
void UpdateMyNormalStruct (
[in] valuetype ConsoleApp4InTest.MyNormalStruct& myStruct
) cil managed
{
.param [1]
.custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
01 00 00 00
)
// Method begins at RVA 0x2164
// Code size 18 (0x12)
.maxstack 2
.locals init (
[0] 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

您会发现,在 IL_0002IL_0007IL_0008 这几行,仍然创建了一个 MyNormalStruct 结构体的防御性副本(defensive copy)。虽然在调用方法 UpdateMyNormalStruct 时以引用的方式传递参数,但在方法体中调用结构体自身的 UpdateValue 前,却创建了一个该结构体的防御性副本,改变的是该副本的 Value。这就有点奇怪了,不是吗?

我们使用 in 参数的目的就是想减少结构体的复制从而提升性能,但这里并没有起到作用。甚至,假如 UpdateMyNormalStruct 方法中多次调用该结构体的非只读方法,编译器也会多次创建该结构体的防御性副本,这就对性能产生了负面影响。

Google 了一些资料是这么解释的:C# 无法知道当它调用一个结构体上的方法(或getter)时,是否也会修改它的值/状态。于是,它所做的就是创建所谓的“防御性副本”。当在结构体上运行方法(或getter)时,它会创建传入的结构体的副本,并在副本上运行方法。这意味着原始副本与传入时完全相同,调用者传入的值并没有被修改。

有没有办法让方法 UpdateMyNormalStruct 调用后输出 8 呢?您将参数改成 ref 修饰符试试看

综上所述,最好不要把 in 修饰符和一般(非只读)结构体一起使用,以免产生晦涩难懂的行为,而且可能对性能产生负面影响。

in 参数的限制

不能将 inrefout 关键字用于以下几种方法:

  • 异步方法,通过使用 async 修饰符定义。
  • 迭代器方法,包括 yield returnyield break 语句。
  • 扩展方法的第一个参数不能有 in 修饰符,除非该参数是结构体。
  • 扩展方法的第一个参数,其中该参数是泛型类型(即使该类型被约束为结构体。)

总结

  • 使用 in 参数,有助于明确表明此参数不可修改的意图。
  • 只读结构体(readonly struct的大小大于 IntPtr.Size [3] 时,出于性能原因,应将其作为 in 参数传递。
  • 不要将一般(非只读)结构体作为 in 参数,因为结构体是可变的,反而有可能对性能产生负面影响,并且可能产生晦涩难懂的行为。

作者 : 技术译民

出品 : 技术译站


  1. https://www.cnblogs.com/ittranslator/p/13876180.html C# 中的只读结构体

  2. https://www.cnblogs.com/ittranslator/p/13664383.html C# 中 Struct 和 Class 的区别总结

  3. https://docs.microsoft.com/zh-cn/dotnet/api/system.intptr.size#System_IntPtr_Size IntPtr.Size

C# 中的 in 参数和性能分析的更多相关文章

  1. SQL中利用DMV进行数据库性能分析

    相信朋友对SQL Server性能调优相关的知识或多或少都有一些了解.虽然说现在NOSQL相关的技术非常的火热,但是RMDB(关系型数据库)与NOSQL是并存的,并且适用在各种的项目中.在一般的企业级 ...

  2. 向mysql中批量插入数据的性能分析

    MYSQL批量插入数据库实现语句性能分析 假定我们的表结构如下 代码如下   CREATE TABLE example (example_id INT NOT NULL,name VARCHAR( 5 ...

  3. 【java基础 17】集合中各实现类的性能分析

    大致的再回顾一下java集合框架的基本情况 一.各Set实现类的性能分析 1.1,HashSet用于添加.查询 HashSet和TreeSet是Set的两个典型实现,HashSet的性能总是比Tree ...

  4. Java中ArrayList和LinkedList的性能分析

    ArrayList和LinkedList是Java集合框架中经常使用的类.如果你只知道从基本性能比较ArrayList和LinkedList,那么请仔细阅读这篇文章. ArrayList应该在需要更多 ...

  5. Graphic32中TBitmap32.TextOut性能分析[转载]

    转载:http://blog.csdn.net/avan_lau/article/details/6958497 最近在分析软件中画线效率问题,发现在画一些标志性符号的方法,存在瓶颈,占用较大的时间. ...

  6. 实例分析ASP.NET在MVC5中使用MiniProfiler监控MVC性能的方法 

    这篇文章主要为大家详细介绍了ASP.NET MVC5使用MiniProfiler监控MVC性能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 MiniProfiler ,一个简单而有效的迷你剖析器 ...

  7. x86服务器中网络性能分析与调优 转

    x86服务器中网络性能分析与调优 2017-04-05 巨枫 英特尔精英汇 [OpenStack 易经]是 EasyStack 官微在2017年新推出的技术品牌,将原创技术干货分享给您,本期我们讨论 ...

  8. 浅谈c#的三个高级参数ref out 和Params C#中is与as的区别分析 “登陆”与“登录”有何区别 经典SQL语句大全(绝对的经典)

    浅谈c#的三个高级参数ref out 和Params   c#的三个高级参数ref out 和Params 前言:在我们学习c#基础的时候,我们会学习到c#的三个高级的参数,分别是out .ref 和 ...

  9. C++ 中数组做参数的分析

    C++ 中数组做参数的分析 1.数组降价问题? "数组引用"以避免"数组降阶",数组降阶是个讨厌的事,这在C语言中是个无法解决的问题,先看一段代码,了解什么是& ...

随机推荐

  1. 我们解决了如何将视频转换为HEVC / H.265和AVC / H.264

    LEADTOOLS Recognition Imaging SDK是精选的LEADTOOLS SDK功能集,旨在在企业级文档自动化解决方案中构建端到端文档成像应用程序,这些解决方案需要OCR,MICR ...

  2. Windows10下JDK8的下载安装与环境变量的配置

    Windows10下JDK8的下载安装与环境变量的配置 下载JDK8(64位) 链接:https://pan.baidu.com/s/10ZMK7NB68kPORZsPOhivog 提取码:agsa ...

  3. I2C 方式

    转自:http://www.cnblogs.com/lucky-apple/archive/2008/07/03/1234581.html 区别: SPI:高速同步串行口.3-4线接口,收发独立.可同 ...

  4. 6个LED的控制

    控制任务和要求 让6个LED按要求工作 电路设计 程序设计 1 int Led1 = 1; //各LED与实验板的联接引脚 2 int Led2 = 2; 3 int Led3 = 3; 4 int ...

  5. 序列化的JavaScript

    下载 序列化的JavaScript序列化的JavaScript 将JavaScript序列化为包含正则表达式.日期和函数的JSON超集. 概述 这个包中的代码最初是作为表示状态的内部模块.为了扩展它的 ...

  6. SpringBoot 完整学习笔记免费分享

    从0到进阶,完全系统性的学习笔记 每次我都会反复拿来观看,因为我们总会有遗漏忘记的地方,但是笔记不会. 希望大家能好好利用它,以下是笔记截图! 以上只是其中的一项部分,这份笔记可以说含金量超高,绝对会 ...

  7. Linux最常用的命令大全

    Linux最常用的命令大全 按功能索引 目录处理命令 ls mkdir pwd cd rmdir cp mv rm 文件处理命令 touch cat tac more less head tail l ...

  8. day26 Pyhton 面向对象复习

    一 class 类名(): pass 对象 object 对象 = 类名() class Person: pass print(Person)#<class '__main__.Person'& ...

  9. MATLAB利用solve函数解多元一次方程组

    matlab求解多元方程组示例: syms k1 k2 k3; [k1 k2 k3] = solve(-3-k3==6, 2-k1-k2+2*k3==11, 2*k1+k2-k3+1==6)或者用[k ...

  10. 习题3-3 数数字(Digit Counting , ACM/ICPC Danang 2007, UVa1225)

    #include<stdio.h> #include<string.h> int main() { char s[100]; scanf("%s",s); ...