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
{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: nopIL_0001: ldarg.0IL_0002: ldobj ConsoleApp4InTest.MyNormalStruct IL_0007: stloc.0IL_0008: ldloca.s 0IL_000a: ldc.i4.8IL_000b: call instance void ConsoleApp4InTest.MyNormalStruct::UpdateValue(int32)IL_0010: nopIL_0011: ret
} // end of method Program::UpdateMyNormalStruct

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

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

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

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

in 参数的限制

不能将 inref 和 out 关键字用于以下几种方法:

  • 异步方法,通过使用 async 修饰符定义。

  • 迭代器方法,包括 yield return 或 yield break 语句。

  • 扩展方法的第一个参数不能有 in 修饰符,除非该参数是结构体。

  • 扩展方法的第一个参数,其中该参数是泛型类型(即使该类型被约束为结构体。)

总结

  • 使用 in 参数,有助于明确表明此参数不可修改的意图。

  • 只读结构体(readonly struct的大小大于 IntPtr.Size [3] 时,出于性能原因,应将其作为 in 参数传递。

  • 不要将一般(非只读)结构体作为 in 参数,因为结构体是可变的,反而有可能对性能产生负面影响,并且可能产生晦涩难懂的行为。


相关链接:

  1. https://mp.weixin.qq.com/s/wwVZbdY7m7da1nmIKb2jCA C# 中的只读结构体 ↩︎

  2. https://mp.weixin.qq.com/s/wVikRMfc4BbrB6WbDy1gXw C# 中 Struct 和 Class 的区别总结 ↩︎

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

作者 :技术译民  

出品 :技术译站(https://ITTranslator.cn/)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/307105.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

禁用笔记本键盘_如何禁用/启用笔记本内置键盘?

有些小伙伴外接了USB键盘想屏蔽掉笔记本的内置键盘&#xff0c;绞尽脑汁都没有办法禁用&#xff0c;其实方法很简单只需要一个简单的命令即可。1、右键点击左下角开始图标(WinX)&#xff0c;选择Windows Powershell(管理员)。2、在打开的窗口中&#xff0c;输入cmd。3、然后输入…

IdentityServer4系列 | 资源密码凭证模式

一、前言从上一篇关于客户端凭证模式中&#xff0c;我们通过创建一个认证授权访问服务&#xff0c;定义一个API和要访问它的客户端&#xff0c;客户端通过IdentityServer上请求访问令牌&#xff0c;并使用它来控制访问API。其中&#xff0c;我们也注意到了在4.x版本中于之前3.x…

深入探究ASP.NET Core Startup的初始化

前言Startup类相信大家都比较熟悉,在我们使用ASP.NET Core开发过程中经常用到的类&#xff0c;我们通常使用它进行IOC服务注册&#xff0c;配置中间件信息等。虽然它不是必须的&#xff0c;但是将这些操作统一在Startup中做处理&#xff0c;会在实际开发中带来许多方便。当我们…

python远程监控服务器多个日志_python压测+paramiko远程监下载日志+js测试报告

关于压测客户端netty nio压测端package com.nio.test;import io.netty.bootstrap.Bootstrap;import io.netty.buffer.ByteBuf;import io.netty.buffer.Unpooled;import io.netty.channel.ChannelFuture;import io.netty.channel.ChannelInitializer;import io.netty.channel.Ch…

【源码】常用的人脸识别数据库以及上篇性别识别源码

上一篇《使用ML.NET模型生成器来完成图片性别识别》发布后&#xff0c;很多朋友希望得到源码&#xff0c;这里附上地址&#xff1a;https://github.com/xin-lai/GenderRecognition常用的人脸数据库对于部分朋友说&#xff0c;找不到训练的数据&#xff0c;这里也给出部分数据&a…

deb包如何改支持12系统_对一个deb包的解压、修改、重新打包全过程方法

出于多种原因&#xff0c;有的时候需要直接对deb包中的各种文件内容进行修改主要有三个问题需要解决&#xff1a;0、如何将deb包文件进行解包呢&#xff1f;1、修改要修改的文件&#xff1f;2、对修改后的内容进行生成deb包&#xff1f;解包命令为#解压出包中的文件到extract目…

程序员过关斩将--真的可以用版本号的方式来保证MQ消费消息的幂等性?

灵魂拷问MQ消息的消费为什么有时候要求幂等性&#xff1f;你们都说可以用版本号来解决幂等性消费&#xff1f;什么才是消息幂等性消费的根本性问题&#xff1f;随着系统的复杂性不断增加&#xff0c;多数系统都会引入MQ来进行解耦&#xff0c;其实从引入MQ的初衷来说&#xff0…

realloc函_[转载]realloc函数的使用及注意事项(转)

原型&#xff1a;externvoid *realloc(void *mem_address, unsigned int newsize);用法&#xff1a;#include 功能&#xff1a;改变mem_address所指内存区域的大小为newsize长度。说明&#xff1a;如果重新分配成功则返回指向被分配内存的指针&#xff0c;否则返回空指针NULL。…

InfluxDB 2.0 之Flux语法篇

由于项目 IoTSharp 需要支持 InfluxDB &#xff0c; 因此进行了初步尝试&#xff0c; 虽然 Flux 语法初次学习&#xff0c; 但查询语句却似曾相识&#xff0c; 如果改一下 标点符号&#xff0c; 你会完全认为他是 C#的拉姆达表达式&#xff0c; 首先看一下写入数据:using (var…

spring的钩子_spring提供的钩子,你知道哪些

俗话说得好“工欲善其事必先利其器”&#xff0c;现如今springboot与springcloud已成为快速构建web应用的利器。作为一个爪洼工程师&#xff0c;知道如下的spring扩展点&#xff0c;可能会让你编写出扩展性、维护性更高的代码。spring提供的钩子&#xff0c;你知道哪些bean的生…

.Net 5性能改进

起因在.Net Core跳过4.0,避免和先.Net Framework 4.0同名,版本号变为5.0,同时也不在叫.Net Core改为.Net 5(统一的叫法),先看看官方对.Net版本规划.本文主要是根据https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-5/ 翻译而来.不完全翻译.顺序也有所调…

ffmpeg库编译加文字_1.编译ffmpeg库

1.下载ffmpeg#!/bin/bashsource"ffmpeg-4.1"if [ ! -r $source ]thencurl http://ffmpeg.org/releases/${source}.tar.bz2 | tar xj || exit 1ficurl 表示下载&#xff0c;后边跟下载的地址。tar表示解压或者压缩。 x表示解压&#xff0c;j表示是否需要解压bz2压缩包…

C#中形态各异的class

本篇是基本知识&#xff0c;老码农请无视&#xff01;&#xff01;&#xff01;普通静态抽象密封分部修饰关键字无staticabstractsealedpartial构造函数调用时机实例化(new)时内部任意静态成员调用时子类实例化(new)时实例化(new)时实例化(new)时包含成员字段属性方法事件索引器…

mysql 笛卡尔积_Mysql内连接、左连接会出现笛卡尔积的理解

先简单解释一下笛卡尔积。 现在,我们有两个集合A和B。 A = {0,1} B = {2,3,4} 集合 AB 和 BA的结果集就可以分别表示为以下这种形式: AB = {(0,2),(1,2),(0,3),(1,3),(0,4),(1,4)}; BA = {(2,0),(2,1),(3,0),(3,1),(4,0),(4,1)}; 以上AB和BA的结…

开放数字世界中的复杂图数据挑战 —— 以教育与开源场景为例

摘要&#xff1a;开源开放的数字世界开始成为时代的潮流&#xff0c;云原生、数据中台、智能PRA开始成为数字世界中的新一代中流砥柱。随着第四范式的普遍流行&#xff0c;各个行业中的数字化转型都会带了海量的具有无限关联的复杂图数据。本报告将以教育与开源两个场景为例&am…

mysql anyvalue函数_Mysql 的ANY_VALUE()函数和 ONLY_FULL_GROUP_BY 模式

Mysql 的ANY_VALUE()函数和 ONLY_FULL_GROUP_BY 模式1、ONLY_FULL_GROUP_BY 引发在mysql 5.7版本以上进行一些ORDER BY 或者 GROUP BY 时&#xff0c;会出现如下错误[Err] 1055 - Expression #1 of ORDER BY clause is not in GROUP BY clause and contains nonaggregated colu…

在IIS中部署SPA应用,多么痛的领悟!

目前公司的Web项目是SPA应用&#xff0c;采用前后端分离开发&#xff0c;所以有时也会倒腾Vue框架。“前后端应用最终以容器形态、在k8s中部署, 为此我搭建了基于Gitlab flow的Devops流程。在Devops实践中&#xff0c;容器部署成为良方和事实标准。但是在开发和自测阶段&#x…

mysql闪回工具下载_MySQL闪回工具之myflash 和 binlog2sql

实践利用binlog2sql查询两个binlog之间的SQL&#xff1a;必须是两个binlog日志&#xff0c;指定start-file和stop-filebinlog2sql -h127.0.0.1 -P3309 -udba -pxxxxxx -dsakila -t employee --start-filemysql-bin.000112 --stop-filemysql-bin.000113 > /tmp/db.sql利用bin…

MySQL大表优化方案

背景阿里云RDS FOR MySQL&#xff08;MySQL5.7版本&#xff09;数据库业务表每月新增数据量超过千万,随着数据量持续增加,我们业务出现大表慢查询,在业务高峰期主业务表的慢查询需要几十秒严重影响业务方案概述一、数据库设计及索引优化MySQL数据库本身高度灵活&#xff0c;造成…

使用Azure静态Web应用部署Blazor Webassembly应用

上一次演示了如何使用Azure静态web应用部署VUE前端项目&#xff08;使用Azure静态web应用全自动部署VUE站点&#xff09;。我们知道静态web应用支持VUE&#xff0c;react&#xff0c;angular等项目的部署。除了支持这些常见前端框架&#xff0c;静态web应用同样支持微软推出的最…