在 C# 7.2 提供了一系列的方法用于方法参数传输的时候减少对结构体的复制从而可以高效使用内存同时提高性能

在开始阅读之前,希望读者对 C# 的值类型、引用类型有比较深刻的认知。

在 C# 中,如果对内存有严格的要求,同时需要减少 GC 的情况,推荐此时使用结构体。但是结构体有一个缺点在于,结构体在每次调用方法作为参数传递的时候都会新建一个副本,这对于性能要求特别高的情况是不适合的。

定义一个值类型

  1. struct Int256
  2. {
  3. public Int256(long bits0, long bits1, long bits2, long bits3)
  4. {
  5. Bits0 = bits0;
  6. Bits1 = bits1;
  7. Bits2 = bits2;
  8. Bits3 = bits3;
  9. }
  10. public long Bits0 { set; get; }
  11. public long Bits1 { get; }
  12. public long Bits2 { get; }
  13. public long Bits3 { get; }
  14. }

此时通过一个简单的赋值就可以获取复制

  1. Int256 f1 = new Int256(0, 1, 2, 1);
  2. var f2 = f1;
  3. f2.Bits0 = 2;
  4. Console.WriteLine($"f1.bits0={f1.Bits0} f2.bits0={f2.Bits0}");
  5. //f1.bits0=0 f2.bits0=2

在调用方法的时候也一样,传入参数就是复制一个新的值

  1. static void Main(string[] args)
  2. {
  3. Int256 f1 = new Int256(0, 1, 2, 1);
  4. Foo(f1);
  5. Console.WriteLine($"{f1.Bits0}"); // 0
  6. }
  7. private static void Foo(Int256 foo)
  8. {
  9. foo.Bits0 = 2;
  10. }

对于很小的值类型,如果小于 IntPtr.Size 的传输,会比引用传递的复制速度快,但是对比比较大的值类型,如上面定义的,复制一次需要的时间会比较长

特别是存在很多次的值传递的时候,如下面的代码,会调用 1000 次的值传递。除了性能的问题,还存在堆栈的内存的问题

定义一个很大的值类型,里面包含 10000 个 double 看起来就很大

  1. struct Double10000
  2. {
  3. public double Double0 { get; }
  4. public double Double1 { get; }
  5. public double Double2 { get; }
  6. ……
  7. public double Double9999 { get; }
  8. }

用递归的方式进行调用,运行的时候很快就可以看到堆栈都被申请的值传递使用,同时 CPU 的使用很高

  1. static void Main(string[] args)
  2. {
  3. Double10000 foo = new Double10000();
  4. Foo(foo);
  5. }
  6. private static void Foo(Double10000 foo, int n = 100)
  7. {
  8. if (n == foo.Double0)
  9. {
  10. return;
  11. }
  12. Foo(foo, n - 1);
  13. }

如果可以让值类型和引用一样传递,是不是就可以减少值类型的复制同时减少堆栈的使用,请注意不要纠结值类型是分配在堆中还是栈中的问题,上面的代码更多的是方法的递归

对比内存的使用,更多的时候关心的是运行的速度。添加一些代码用来测试性能,同时减少调用

  1. var st = new Stopwatch();
  2. st.Start();
  3. Foo(foo);
  4. st.Stop();
  5. Console.WriteLine(st.ElapsedTicks);
  1. private static void Foo(Double10000 foo, int n = 10)
  2. {
  3. if (n == foo.Double0)
  4. {
  5. return;
  6. }
  7. Foo(foo, n - 1);
  8. }

这里输出的 ElapsedTicks 的单位是 100ns 需要知道 1ms=1000000ns 也就是 1w 的 tick 就是 1 毫秒,下面我运行 3 次代码,收集到的值

  1. 10991
  2. 14950
  3. 16183

在 C# 7.2 可以使用 in 关键字告诉 VisualStudio 当前的方法不会对传进来的结构体进行修改,当前这样写只是语法层面。如果有一些厉害的黑客,可能还继续这样写入,于是为了防止真的进行修改,在底层还是复制了一份。

也就是只是在参数里面使用了 in 是不够的,具体请看这个拖后腿的“in” - Bean.Hsiang - 博客园

如果想要更好的使用内存同时提高性能,只有在可以被标记为只读的结构体的时候使用 in 才可以

先将 Double100 标记为 readonly 如果一个值类型标记为 readonly 也就无法对里面的字段或属性进行设置了

在 Foo 传入的方法参数标记 in 这样就完成了,因为 in 表示对参数不进行修改,而传入的是 readonly struct 本来就不能被修改,于是就传入 struct 的引用

  1. readonly struct Double10000
  2. private static void Foo(in Double10000 foo, int n = 10)
  3. {
  4. if (n == foo.Double0)
  5. {
  6. return;
  7. }
  8. Foo(foo, n - 1);
  9. }

同样运行 3 次,可以看到速度是原来的 10 倍

  1. 2052
  2. 1837
  3. 1683

同时占用的堆栈更小,可以使用更多的递归,修改 Foo 函数调用次数为 1000 可以看到还能运行,但是如果去掉了参数 in 最多只能调用 20 次

没有加 in 的参数,运行了 17 次

添加了 in 之后因为不需要复制值,减少内存的时候,此时运行了 1000 次递归都可以,在使用in之后速度和使用内存都比较好

在很多次方法调用使用参数的时候,如果传入的值是值类型,如果此时的 struct 里面的属性都是只读属性,推荐将 struct 标记为 readonly 同时在方法参数标记 in 这样可以让 struct 作为引用传递,也就是复制的只是指针,只要 struct 的长度比指针小就推荐这个方法

我搭建了自己的博客 https://lindexi.gitee.io/ 欢迎大家访问,里面有很多新的博客。只有在我看到博客写成熟之后才会放在csdn或博客园,但是一旦发布了就不再更新

如果在博客看到有任何不懂的,欢迎交流,我搭建了 dotnet 职业技术学院 欢迎大家加入


本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接:http://blog.csdn.net/lindexi_gd ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系

C# 7.2 通过 in 和 readonly struct 减少方法值复制提高性能的更多相关文章

  1. C# 中的只读结构体(readonly struct)

    翻译自 John Demetriou 2018年4月8日 的文章 <C# 7.2 – Let's Talk About Readonly Structs>[1] 在本文中,我们来聊一聊从 ...

  2. Swift开发第五篇——四个知识点(Struct Mutable方法&Tuple&autoclosure&Optional Chain)

    本篇分三部分: 一.Struct Mutable方法 二.多元组(Tuple) 的使用 三.autoclosure 的使用 四.Optional Chain 的使用 一.Struct Mutable方 ...

  3. go chapter 10 函数 方法 struct的方法

    1. struct的方法 // 定义struct type MyStruct struct{} // 定义方法 (那个对象可以回调)方法名(参数) 返回值 {} (s *MyStruct) FillS ...

  4. matlab中struct创建方法

    MATLAB中struct创建方法可分为:直接创建法和struct()函数创建法 (1)直接创建: 直接定义字段,像使用一般matlab变量一样,不需要事先声明,支持动态扩充.下面创建一个Studen ...

  5. 解决Hibernate Write operations are not allowed in read-only mode的方法

    错误信息: org.springframework.dao.InvalidDataAccessApiUsageException: Write operations are not allowed i ...

  6. .net TxetBox控件设置ReadOnly=True后台取值问题

    1.为TxetBox添加onfocus=this.blur()进行模拟 2.通过 Request.From["TextBox"].Trim()取值; 3.后台CS文件设置TextB ...

  7. GO入门——6. struct与方法

    1 struct Go 中的struct与C中的struct非常相似,并且Go没有class 使用 type struct{} 定义结构,名称遵循可见性规则 支持指向自身的指针类型成员 支持匿名结构, ...

  8. golang 对struct进行Serialize的方法,即将存取二进制文件到struct的方法

    方法一: serialize 的标准方法: 使用gob 和 base64 或 base58. 方法二: 下面是自己实现的 serialize 方法,不推荐自己实现,应该用标准方法. 代码如下: pac ...

  9. Android源码中中一种常见的struct使用方法

    直接看例子: #include<iostream> #include<stdlib.h> using namespace std; struct Base{ int ba; i ...

随机推荐

  1. NOIP模拟 7.03

    Problem 1 抓牛(catchcow.cpp/c/pas) [题目描述] 农夫约翰被通知,他的一只奶牛逃逸了!所以他决定,马上出发,尽快把那只奶牛抓回来. 他们都站在数轴上.约翰在N(O≤N≤1 ...

  2. goland的下载安装破解并配置

    1.下载地址:https://www.jetbrains.com/go/ 2.安装:简单 3.破解:https://www.cnblogs.com/igoodful/p/9113946.html 4. ...

  3. bnd.bnd属性文件格式

    1.Header以大写字母开头 Bundle-Name: StoreAdminProductsTool 2.Instruction以-和小写字母开头 -sources: true 3. Macro形式 ...

  4. 【JZOJ4894】【NOIP2016提高A组集训第16场11.15】SJR的直线

    题目描述 数据范围 解法 考虑逐次加入每一条直线. 对于当前已加入的直线集合L,现在要新加入一条直线l. 那么它产生的贡献,与平行线有关. 对于任意三条直线,如果其中任意两条平行,那么将不做贡献. 所 ...

  5. 将数组对象转换成DataSet

    public static DataSet ObjectArrayToDataSet(object[] objArr) { if (objArr.Length == 0) return null; D ...

  6. openjudge dp水题记录

    当发现自己竟然不会打dp的时候内心是崩溃的,然后按照一年前的刷题记录刷openjudge,然后发现自己准确率比一年前(刚学信竞两个月时)的准确率低得多,已经没救. 列一下最近打的几道sb题 2985: ...

  7. SqlAlchemy的简单使用

    1.SQLAlchemy SQLAlchemy是python的一个通用的ORM框架 1.1 创建数据表 from sqlalchemy.ext.declarative import declarati ...

  8. execute和submit的区别与联系

    execute和submit都属于线程池的方法,execute只能提交Runnable类型的任务,而submit既能提交Runnable类型任务也能提交Callable类型任务. execute会直接 ...

  9. TIJ——Chapter Three:Operators

    Operators 本章节比较简单,所以简单的做一些笔记: 几个要点: 1.When the compiler sees a String followed by a "+" fo ...

  10. Docker镜像部分详解

    拉取镜像: docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签] Docker 镜像仓库地址:地址的格式一般是 [:端口号].默认地址是 Docke ...