C# Span 源码解读和应用实践

一:背景

1. 讲故事

这两天工作上太忙没有及时持续的文章产出,和大家说声抱歉,前几天群里一个朋友在问什么时候可以产出 Span 的下一篇,哈哈,这就来啦!读过上一篇的朋友应该都知道 Span 统一了 .NET 程序 栈 + 托管 + 非托管 实现了三大块内存的统一访问,🐂👃,而且在 .net 底层 Library 中也是一等公民的存在,很多现有的类都提供了对 Span / ReadOnlySpan 的支持。

  • String 对 Span / ReadOnlySpan 的支持

    public sealed class String
    {
        [MethodImpl(MethodImplOptions.InternalCall)]
        [NullableContext(0)]
        public extern String(ReadOnlySpan<char> value);
    }

  • StringBuilder 对 Span / ReadOnlySpan 的支持

    public sealed class StringBuilder : ISerializable
    {
        public unsafe StringBuilder Append(ReadOnlySpan<char> value)
        {
            if (value.Length > 0)
            {
                fixed (char* value2 = &MemoryMarshal.GetReference(value))
                {
                    Append(value2, value.Length);
                }
            }
            return this;
        }
    }

  • Int 对 Span / ReadOnlySpan 的支持

    public readonly struct Int32
    {
        public static int Parse(ReadOnlySpan<char> s, NumberStyles style = NumberStyles.Integer, IFormatProvider? provider = null)
        {
            NumberFormatInfo.ValidateParseStyleInteger(style);
            return Number.ParseInt32(s, style, NumberFormatInfo.GetInstance(provider));
        }
    }

怎么样,这些通用 & 基础的类都在大力对接 Span / ReadOnlySpan,更别说复杂类型了,其地位不言自明哈,接下来我们就从 Span 本身的机制聊起。

二: Span 原理探究

1. Span 源码分析

灵活运用 Span 解决工作中的实际问题我相信大家应该没什么毛病了,有了这个基础再从 Span 的源码 和 用户态 和大家一起深度剖析,从源码开始吧。


    public readonly ref struct Span<T>
    {
        internal readonly ByReference<T> _pointer;

        private readonly int _length;
    }

上面代码的 ref struct 可以看出,这个 Span 是只可以分配在栈上的值类型,然后就是里面的 _pointer 和 _length 两个实例字段,不知道看完这两个字段脑子里是不是有一幅图,大概是这样的。

可以清晰的看出,Span 就是用来映射一段可以连续访问的内存地址,空间大小由 length 控制,开始位置由 _pointer 指定,是不是像极了指针😁😁😁,是的,语言团队要保证你的程序高性能,还得照护你的人身安全,出了各种手段,真是煞费苦心! 👍👍👍

2. Span 用户态分析

虽然图已经画了,但还是有很多朋友希望眼见为实,必须实操演练,嘿嘿,无惧任何挑战,那我先把上面的图化成代码:


        static void Main(string[] args)
        {
            var nums = new int[] { 1, 2, 3, 4, 5, 6 };

            var span = new Span<int>(nums);

            Console.ReadLine();
        }

接下来我用 windbg 把线程栈中的 span 也找出来。


0:000> !clrstack -l
OS Thread Id: 0x181c (0)
        Child SP               IP Call Site
000000963277E5D0 00007ffc3e601434 ConsoleApp1.Program.Main(System.String[]) [E:\net5\ConsoleApp2\ConsoleApp1\Program.cs @ 13]
    LOCALS:
        0x000000963277E618 = 0x000001e956b8ab10
        0x000000963277E608 = 0x000001e956b8ab20

从最后一行代码可以看出:span 的栈地址是 0x000000963277E608,栈内容是:0x000001e956b8ab20,按照图的理论: 0x000001e956b8ab20 应该是 nums 数组元素 1 的内存地址,可以用 dp 验证一下。


0:000> dp 0x000001e956b8ab20
000001e9`56b8ab20  00000002`00000001 00000004`00000003
000001e9`56b8ab30  00000006`00000005 00000000`00000000
000001e9`56b8ab40  00007ffc`3e6c4388 00000000`00000000

从上面三行内存地址来看,数组的:1,2,3,4,5,6 依次排列,有些朋友可能有点小疑问,为啥 nums 的内存地址不是指向数组元素 1 的呢? 那我来普及一下吧,先用 dp 唤出数组的内存地址。


0:000> dp 0x000001e956b8ab10
000001e9`56b8ab10  00007ffc`3e69f090 00000000`00000006
000001e9`56b8ab20  00000002`00000001 00000004`00000003
000001e9`56b8ab30  00000006`00000005 00000000`00000000

可以看出,第一排为: 00007ffc3e69f090 0000000000000006, 前面的 8 byte 表示 数组 的 方法表地址,后面的 8byte 表示 6 ,也就是说数组有 6个元素,不信的话我截一张图:

span 是由 _pointer + length 组成的,刚才的 _pointer 也给大家演示了,那 length 的值在哪里呢? 因为 span 是 struct,所以需要用 dp 把刚才的线程栈最小的栈地址打出来就可以了。

到这里,我觉得我讲的已经够清楚了,如果还有点懵的话可以仔细想一想哈。

三:Span 在 String 和 List 的实践

Span的应用场景真的是太多了,不可能在这篇一一列举,这里我就举两个例子吧,让大家能够感受到 Span 的强大即可。

1. 在 String 上的应用

案例:如何高效的计算出用户输入的值 10+20 ?

1) 传统 Substring 做法

传统的做法很简单,截取呗,代码如下:


        static void Main(string[] args)
        {
            var word = "10+20";

            var splitIndex = word.IndexOf("+");

            var num1 = int.Parse(word.Substring(0, splitIndex));

            var num2 = int.Parse(word.Substring(splitIndex + 1));

            var sum = num1 + num2;

            Console.WriteLine($"{num1}+{num2}={sum}");

            Console.ReadLine();
        }

结果是很轻松的算出来了,但你仔细想想这里是不是有点什么问题,比如说为了从 word 中扣出 num,我用了两次 SubString,就意味着会在 托管堆 上生成两个 string,如果说我执行 1w 次话,那托管堆上会不会有 2w 个 string 呢? 修改代码如下:


            for (int i = 0; i < 10000; i++)
            {
                var num1 = int.Parse(word.Substring(0, splitIndex));

                var num2 = int.Parse(word.Substring(splitIndex + 1));

                var sum = num1 + num2; 
            }

然后看一下 托管堆 上 String 的个数


0:000> !dumpheap -type String -stat
Statistics:
              MT    Count    TotalSize Class Name
00007ffc53a81e18    20167       556538 System.String

托管堆上有 20167 个,挺恐怖的,真的是给 GC 添麻烦哈,这里还有 167 个是系统自带的,接下来的问题是有没有办法替换 SubString 从而不生成临时string呢?

2) 新式 Span 做法

如果看懂了 Span 结构图,你就应该会使用 _pointer + length 将 string 进行切片处理,对不对,代码如下:


            for (int i = 0; i < 10000; i++)
            {
                var num1 = int.Parse(word.AsSpan(0, splitIndex));

                var num2 = int.Parse(word.AsSpan(splitIndex));

                var sum = num1 + num2; 
            }

然后在 托管堆 验证一下,是不是没有 临时 string 了?


0:000> !dumpheap -type String -stat
Statistics:
              MT    Count    TotalSize Class Name
00007ffc53a51e18      167        36538 System.String

可以看到就只有 167 个系统字符串,性能也得到了不小的提升,🐂👃🦆。

2. 在 List 上的应用

平时用 Span 的时候,更多的会应用到 Array 上面,毕竟 Array 在托管堆上是连续内存,方便 Span 在上面画一个可视窗口,其实不仅仅是 Array,从 .NET5 开始在 List 上画一个视图也是可以的,截图如下:

因为 List 的 CURD 会导致底层的 Array 忽长忽短或重新分配,也就无法实现物理上的连续内存,所以 Span 应用到 List 之后,希望List是不可变的,这也是官方的建议。

四:总结

总的来说,Span 在 .NET 底层框架中的地位是越来越显著了,相信 netCore 追求更高更快的性能上 Span 一定大有可为,大家赶紧学起来,😀😀😀

更多高质量干货:参见我的 GitHub: dotnetfly

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,039评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,426评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,417评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,868评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,892评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,692评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,416评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,326评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,782评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,957评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,102评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,790评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,442评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,996评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,113评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,332评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,044评论 2 355

推荐阅读更多精彩内容