C# 中的 in 参数和性能分析
in 修饰符也是从 C# 7.2 开始引入的,它与我们上一篇中讨论的 《C# 中的只读结构体(readonly struct)》[1] 是紧密相关的。
in 修饰符
in 修饰符通过引用传递参数。 它让形参成为实参的别名,即对形参执行的任何操作都是对实参执行的。 它类似于 ref 或 out 关键字,不同之处在于 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 中先调用了结构体自身的方法 UpdateValue 将 Value 修改为 2, 再调用 Program 中的方法 UpdateMyNormalStruct, 而该方法中又调用了 MyNormalStruct 结构体自身的方法 UpdateValue,那么输出是不是应该是 8 呢? 如果您这么想就错了。
它的正确输出结果是 2,这是为什么呢?
这是因为,结构体和许多内置的简单类型(sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool 和 enum 类型)一样,都是值类型,在传递参数的时候以值的方式传递。因此调用方法 UpdateMyNormalStruct 时传递的是 myStruct 变量的新副本,在此方法中,其实是此副本调用了 UpdateValue 方法,所以原变量 myStruct 的 Value 不会发生变化。
说到这里,有聪明的朋友可能会想,我们给 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_0002、IL_0007 和 IL_0008 这几行,仍然创建了一个 MyNormalStruct 结构体的防御性副本(defensive copy)。虽然在调用方法 UpdateMyNormalStruct 时以引用的方式传递参数,但在方法体中调用结构体自身的 UpdateValue 前,却创建了一个该结构体的防御性副本,改变的是该副本的 Value。这就有点奇怪了,不是吗?
我们使用 in 参数的目的就是想减少结构体的复制从而提升性能,但这里并没有起到作用。甚至,假如 UpdateMyNormalStruct 方法中多次调用该结构体的非只读方法,编译器也会多次创建该结构体的防御性副本,这就对性能产生了负面影响。
Google 了一些资料是这么解释的:C# 无法知道当它调用一个结构体上的方法(或getter)时,是否也会修改它的值/状态。于是,它所做的就是创建所谓的“防御性副本”。当在结构体上运行方法(或getter)时,它会创建传入的结构体的副本,并在副本上运行方法。这意味着原始副本与传入时完全相同,调用者传入的值并没有被修改。
有没有办法让方法 UpdateMyNormalStruct 调用后输出 8 呢?您将参数改成 ref 修饰符试试看
综上所述,最好不要把 in 修饰符和一般(非只读)结构体一起使用,以免产生晦涩难懂的行为,而且可能对性能产生负面影响。
in 参数的限制
不能将 in、ref 和 out 关键字用于以下几种方法:
- 异步方法,通过使用 async修饰符定义。
- 迭代器方法,包括 yield return或yield break语句。
- 扩展方法的第一个参数不能有 in修饰符,除非该参数是结构体。
- 扩展方法的第一个参数,其中该参数是泛型类型(即使该类型被约束为结构体。)
总结
- 使用 in参数,有助于明确表明此参数不可修改的意图。
- 当只读结构体(readonly struct)的大小大于IntPtr.Size[3] 时,出于性能原因,应将其作为in参数传递。
- 不要将一般(非只读)结构体作为 in参数,因为结构体是可变的,反而有可能对性能产生负面影响,并且可能产生晦涩难懂的行为。
作者 : 技术译民
出品 : 技术译站
- https://www.cnblogs.com/ittranslator/p/13876180.html C# 中的只读结构体 ︎ 
- https://www.cnblogs.com/ittranslator/p/13664383.html C# 中 Struct 和 Class 的区别总结 ︎ 
- https://docs.microsoft.com/zh-cn/dotnet/api/system.intptr.size#System_IntPtr_Size IntPtr.Size ︎ 
C# 中的 in 参数和性能分析的更多相关文章
- SQL中利用DMV进行数据库性能分析
		相信朋友对SQL Server性能调优相关的知识或多或少都有一些了解.虽然说现在NOSQL相关的技术非常的火热,但是RMDB(关系型数据库)与NOSQL是并存的,并且适用在各种的项目中.在一般的企业级 ... 
- 向mysql中批量插入数据的性能分析
		MYSQL批量插入数据库实现语句性能分析 假定我们的表结构如下 代码如下 CREATE TABLE example (example_id INT NOT NULL,name VARCHAR( 5 ... 
- 【java基础 17】集合中各实现类的性能分析
		大致的再回顾一下java集合框架的基本情况 一.各Set实现类的性能分析 1.1,HashSet用于添加.查询 HashSet和TreeSet是Set的两个典型实现,HashSet的性能总是比Tree ... 
- Java中ArrayList和LinkedList的性能分析
		ArrayList和LinkedList是Java集合框架中经常使用的类.如果你只知道从基本性能比较ArrayList和LinkedList,那么请仔细阅读这篇文章. ArrayList应该在需要更多 ... 
- Graphic32中TBitmap32.TextOut性能分析[转载]
		转载:http://blog.csdn.net/avan_lau/article/details/6958497 最近在分析软件中画线效率问题,发现在画一些标志性符号的方法,存在瓶颈,占用较大的时间. ... 
- 实例分析ASP.NET在MVC5中使用MiniProfiler监控MVC性能的方法 
		这篇文章主要为大家详细介绍了ASP.NET MVC5使用MiniProfiler监控MVC性能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 MiniProfiler ,一个简单而有效的迷你剖析器 ... 
- x86服务器中网络性能分析与调优 转
		x86服务器中网络性能分析与调优 2017-04-05 巨枫 英特尔精英汇 [OpenStack 易经]是 EasyStack 官微在2017年新推出的技术品牌,将原创技术干货分享给您,本期我们讨论 ... 
- 浅谈c#的三个高级参数ref out 和Params    C#中is与as的区别分析      “登陆”与“登录”有何区别    经典SQL语句大全(绝对的经典)
		浅谈c#的三个高级参数ref out 和Params c#的三个高级参数ref out 和Params 前言:在我们学习c#基础的时候,我们会学习到c#的三个高级的参数,分别是out .ref 和 ... 
- C++ 中数组做参数的分析
		C++ 中数组做参数的分析 1.数组降价问题? "数组引用"以避免"数组降阶",数组降阶是个讨厌的事,这在C语言中是个无法解决的问题,先看一段代码,了解什么是& ... 
随机推荐
- nginx 1.12 负载均衡配置
			负载均衡策略有以下几种: 请求轮询:round-robin,是默认策略,应用服务器的请求以循环方式分发,可以设置权重weight,默认权重均为1,因此每台后端服务器接受的请求数相同. 最少连接:lea ... 
- Python-joypy和 R-ggridges 峰峦图制作
			本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 以下文章来源于DataCharm,作者 宁海涛 转载地址 https://www ... 
- 1个LED的亮度自动控制
			控制任务和要求 通过程序控制LED的亮度按照要求变化 电路设计 程序设计 1 int bright_number = 0; //此变量用来表示LED的亮度 2 int bright_gap = 5; ... 
- javaagent+asm破解censum
			内容介绍 最近在学习字节码相关知识,了解到通过ASM字节码改写技术来做破解一些软件破解,非常感兴趣,本文记录一下破解 Censum的过程(仅个人学习使用). 之前也写过一篇暴力破解Censum的文章, ... 
- JMeter实战(一) 体系结构
			此为开篇,介绍JMeter的组成结构,阅读后对JMeter形成整体认知和初步印象. 为了便于后续讲解,先明确下2个术语. 元件:如HTTP请求.事务控制器.响应断言,就是一个元件. 组件:如逻辑控制器 ... 
- CDH5部署三部曲之一:准备工作
			欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ... 
- YCM 安装小记
			layout: post title: YCM 安装小记 半夜,女朋友在那边抱怨购物车的物品秒无货,我这边刚好成功安装了vim上最难装的插件--YouCompleteMe,内心非常激动,于是本着取之于 ... 
- ASP课程实例1——简易的手机号抽奖
			本程序用到了最基本的vbscript函数. 请大家注意它们的用法并熟悉asp网页的基本结构. inputbox,mid() ,replace(),rnd(),fix(),document.write ... 
- 多测师讲解python _练习题003_高级讲师肖sir
			python 003作业题:# 1.分别打印100以内的所有偶数和奇数并存入不同的列表当中# 2.请写一段Python代码实现删除一个list = [1, 3, 6, 9, 1, 8]# 里面的重复元 ... 
- K8S节点异常怎么办?TKE"节点健康检查和自愈"来帮忙
			节点健康检测 意义 在K8S集群运行的过程中,节点常常会因为运行时组件的问题.内核死锁.资源不足等各种各样的原因不可用.Kubelet默认对节点的PIDPressure.MemoryPressure. ... 
