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

in 修饰符

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

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

举个简单的例子:

  1. struct Product
  2. {
  3. public int ProductId { get; set; }
  4. public string ProductName { get; set; }
  5. }
  6. public static void Modify(in Product product)
  7. {
  8. //product = new Product(); // 错误 CS8331 无法分配到 变量 'in Product',因为它是只读变量
  9. //product.ProductName = "测试商品"; // 错误 CS8332 不能分配到 变量 'in Product' 的成员,因为它是只读变量
  10. Console.WriteLine($"Id: {product.ProductId}, Name: {product.ProductName}"); // OK
  11. }

引入 in 参数的原因

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

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

in 参数对性能的提升

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

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

代码如下所示:

  1. public struct NormalStruct
  2. {
  3. public decimal Number1 { get; set; }
  4. public decimal Number2 { get; set; }
  5. //...
  6. public decimal Number30 { get; set; }
  7. }
  8. public readonly struct ReadOnlyStruct
  9. {
  10. // 自动属性上的 readonly 关键字是可以省略的,这里加上是为了便于理解
  11. public readonly decimal Number1 { get; }
  12. public readonly decimal Number2 { get; }
  13. //...
  14. public readonly decimal Number30 { get; }
  15. }
  16. public class BenchmarkClass
  17. {
  18. const int loops = 50000000;
  19. NormalStruct normalInstance = new NormalStruct();
  20. ReadOnlyStruct readOnlyInstance = new ReadOnlyStruct();
  21. [Benchmark(Baseline = true)]
  22. public decimal DoNormalLoop()
  23. {
  24. decimal result = 0M;
  25. for (int i = 0; i < loops; i++)
  26. {
  27. result = Compute(normalInstance);
  28. }
  29. return result;
  30. }
  31. [Benchmark]
  32. public decimal DoNormalLoopByIn()
  33. {
  34. decimal result = 0M;
  35. for (int i = 0; i < loops; i++)
  36. {
  37. result = ComputeIn(in normalInstance);
  38. }
  39. return result;
  40. }
  41. [Benchmark]
  42. public decimal DoReadOnlyLoopByIn()
  43. {
  44. decimal result = 0M;
  45. for (int i = 0; i < loops; i++)
  46. {
  47. result = ComputeIn(in readOnlyInstance);
  48. }
  49. return result;
  50. }
  51. public decimal Compute(NormalStruct s)
  52. {
  53. //业务逻辑...
  54. return 0M;
  55. }
  56. public decimal ComputeIn(in NormalStruct s)
  57. {
  58. //业务逻辑...
  59. return 0M;
  60. }
  61. public decimal ComputeIn(in ReadOnlyStruct s)
  62. {
  63. //业务逻辑...
  64. return 0M;
  65. }
  66. }

在没有使用 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 来实现,代码如下:

  1. [Benchmark(Baseline = true)]
  2. public decimal DoNormalLoop()
  3. {
  4. decimal result = 0M;
  5. Parallel.For(0, loops, i => Compute(normalInstance));
  6. return result;
  7. }
  8. [Benchmark]
  9. public decimal DoNormalLoopByIn()
  10. {
  11. decimal result = 0M;
  12. Parallel.For(0, loops, i => ComputeIn(in normalInstance));
  13. return result;
  14. }
  15. [Benchmark]
  16. public decimal DoReadOnlyLoopByIn()
  17. {
  18. decimal result = 0M;
  19. Parallel.For(0, loops, i => ComputeIn(in readOnlyInstance));
  20. return result;
  21. }

事实上,道理是一样的,在没有使用 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

代码如下:

  1. struct MyNormalStruct
  2. {
  3. public int Value { get; set; }
  4. public void UpdateValue(int value)
  5. {
  6. Value = value;
  7. }
  8. }
  9. class Program
  10. {
  11. static void UpdateMyNormalStruct(MyNormalStruct myStruct)
  12. {
  13. myStruct.UpdateValue(8);
  14. }
  15. static void Main(string[] args)
  16. {
  17. MyNormalStruct myStruct = new MyNormalStruct();
  18. myStruct.UpdateValue(2);
  19. UpdateMyNormalStruct(myStruct);
  20. Console.WriteLine(myStruct.Value);
  21. }
  22. }

您可以猜想一下它的运行结果是什么呢? 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 参数不就是引用传递吗?

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

  1. static void UpdateMyNormalStruct(in MyNormalStruct myStruct)
  2. {
  3. myStruct.UpdateValue(8);
  4. }
  5. static void Main(string[] args)
  6. {
  7. MyNormalStruct myStruct = new MyNormalStruct();
  8. myStruct.UpdateValue(2);
  9. UpdateMyNormalStruct(in myStruct);
  10. Console.WriteLine(myStruct.Value);
  11. }

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

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

  1. .method private hidebysig static
  2. void UpdateMyNormalStruct (
  3. [in] valuetype ConsoleApp4InTest.MyNormalStruct& myStruct
  4. ) cil managed
  5. {
  6. .param [1]
  7. .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
  8. 01 00 00 00
  9. )
  10. // Method begins at RVA 0x2164
  11. // Code size 18 (0x12)
  12. .maxstack 2
  13. .locals init (
  14. [0] valuetype ConsoleApp4InTest.MyNormalStruct
  15. )
  16. IL_0000: nop
  17. IL_0001: ldarg.0
  18. IL_0002: ldobj ConsoleApp4InTest.MyNormalStruct
  19. IL_0007: stloc.0
  20. IL_0008: ldloca.s 0
  21. IL_000a: ldc.i4.8
  22. IL_000b: call instance void ConsoleApp4InTest.MyNormalStruct::UpdateValue(int32)
  23. IL_0010: nop
  24. IL_0011: ret
  25. } // 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. mysql-1-select

    #进阶1:基础查询 /* 语法: SELECT 查询列表 FROM 表名; 特点: 1.查询列表可以是:表中字段.常量值.表达式.函数 2.查询的结果是一个虚拟的表格 */ USE myemploye ...

  2. Django-Scrapy生成后端json接口

    Django-Scrapy生成后端json接口: 网上的关于django-scrapy的介绍比较少,该博客只在本人查资料的过程中学习的,如果不对之处,希望指出改正: 以后的博客可能不会再出关于djan ...

  3. Python练习题 041:Project Euler 013:求和、取前10位数值

    本题来自 Project Euler 第13题:https://projecteuler.net/problem=13 # Project Euler: Problem 13: Large sum # ...

  4. Linux常用命令代码大全

    arch 显示机器的处理器架构(1) uname -m 显示机器的处理器架构(2) uname -r 显示正在使用的内核版本 dmidecode -q 显示硬件系统部件 – (SMBIOS / DMI ...

  5. 实验 6:OpenDaylight 实验——OpenDaylight 及 Postman 实现流表下发

    一.实验目的 熟悉 Postman 的使用;熟悉如何使用 OpenDaylight 通过 Postman 下发流表. 二.实验任务 流表有软超时和硬超时的概念,分别对应流表中的 idle_timeou ...

  6. ByPass Mode(略过模式或旁路模式)

    参考: 1. https://baike.baidu.com/item/%E6%97%81%E8%B7%AF%E6%A8%A1%E5%BC%8F/3120563 2. https://zhidao.b ...

  7. STM32F103C8T6驱动WS2812b灯条

    STM32F103C8T6驱动WS2812b灯条 几天小朋友到别人家玩,看上了人家的金鱼,人家就给了她一条小金鱼,有了小金鱼,怕它没氧气挂掉,买了一个氧气泵,没有东西喂它也不行,又买了一包鱼料,又因为 ...

  8. SpringBoot+Activiti+bpmn.js+Vue.js+Elementui(OA系统审批流)

    引言:OA系统用到请假.加班.调休.离职,需要使用工作流进行流程审批 一:activiti流程设计器的选择(通过学习activiti工作流过程中,发现一款好的流程设计器将会更好的方便的设计好流程(主要 ...

  9. 不要以为Bug写的好就是好程序员,其实这只占不到15%!

      最近和一位从事多年架构工作的技术哥们见面,聊到了近期面试程序员的一些经历,谈到了"如何判断程序员水平高低"这个话题,颇有些感触,觉得有价值,因此花了些时间整理.分享给大家. 正 ...

  10. js 无刷新文件上传 (兼容IE9 )

    之前项目中有个文件上传了需求,于是直接就使用了FormData对象异步上传,但是在测试得时候发现ie9无法正常上传(项目要求兼容IE9+),无奈,查资料得知IE9- 版本不支持formdata对象得异 ...