C#如何通过静态委托提升性能

起因

说是<<String.Create真的快吗?>>的后续,其实关系不是很大,只是使用的代码示例是有关系的,更多的是如何高效使用委托.最近将文章迁移了一部分到今日头条,其中一篇<<写更好的C#代码>>,@Alsein评论在.Net高版本可以使用静态委托,可以避免多余的内存分配,从而提高委托的性能.

先看看测试代码

using System;
using System.Buffers;
using BenchmarkDotNet.Attributes;

namespace CSharpBenchmarks.StringTest
{
    [MemoryDiagnoser]
    [DisassemblyDiagnoser(printSource: true)]
    public class StringCreateTest
    {
        [Params(4096)]
        public int Count { get; set; }

        public static string OrderNum = "FA210609";

        [Benchmark(Baseline = true)]
        public void ArrayTest()
        {
            for (int i = 0; i < Count; i++)
            {
                Array(OrderNum);
            }
        }

        [Benchmark]
        public void CreateTest()
        {
            for (int i = 0; i < Count; i++)
            {
                Create(OrderNum);
            }
        }

        [Benchmark]
        public void LocalCreateTest()
        {
            for (int i = 0; i < Count; i++)
            {
                LocalCreate(OrderNum);
            }
        }

        [Benchmark]
        public void StaticCreateTest()
        {
            for (int i = 0; i < Count; i++)
            {
                StaticCreate(OrderNum);
            }
        }


        [Benchmark]
        public void CreateCaheTest()
        {
            for (int i = 0; i < Count; i++)
            {
                CreateCache(OrderNum);
            }
        }

        public string Array(string input)
        {
            int len = input.Length;
            int postion = -1;
            char[] arr = new char[len];
            for (int i = 0; i < len; i++)
            {
                if (input[i] >= 'A' && input[i] <= 'Z')
                {
                    arr[i] = input[i];
                }
                else
                {
                    postion = i;
                    break;
                }
            }

            if (postion > 0)
            {
                arr[postion] = 'R';
            }

            int start = postion + 1;
            for (int i = start; i < len; i++)
            {
                arr[i] = input[i];
            }

            return new string(arr, 0, len);
        }

        public string Create(string input)
        {
            int len = input.Length;
            return string.Create(len, input, (target, src) =>
            {
                int postion = -1;
                for (int i = 0; i < len; i++)
                {
                    if (src[i] >= 'A' && src[i] <= 'Z')
                    {
                        target[i] = src[i];
                    }
                    else
                    {
                        postion = i;
                        break;
                    }
                }

                if (postion > 0)
                {
                    target[postion] = 'R';
                }

                int start = postion + 1;
                for (int i = start; i < len; i++)
                {
                    target[i] = src[i];
                }
            });
        }

        /// <summary>
        /// 减少使用外部变量
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public string LocalCreate(string input)
        {
            int len = input.Length;
            return string.Create(len, input, (target, src) =>
            {
                int postion = -1;
                int strLen = target.Length;
                for (int i = 0; i < strLen; i++)
                {
                    if (src[i] >= 'A' && src[i] <= 'Z')
                    {
                        target[i] = src[i];
                    }
                    else
                    {
                        postion = i;
                        break;
                    }
                }

                if (postion > 0)
                {
                    target[postion] = 'R';
                }

                int start = postion + 1;
                for (int i = start; i < strLen; i++)
                {
                    target[i] = src[i];
                }
            });
        }

        /// <summary>
        /// 使用静态匿名函数
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public string StaticCreate(string input)
        {
            int len = input.Length;
            //在委托前加入static关键字修饰,主要避免创建多余的委托对象
            //在C# 9加入,静态匿名函数(包含lambda和匿名函数)
            return string.Create(len, input, static (target, src) =>
            {
                int postion = -1;
                int strLen = target.Length;
                for (int i = 0; i < strLen; i++)
                {
                    if (src[i] >= 'A' && src[i] <= 'Z')
                    {
                        target[i] = src[i];
                    }
                    else
                    {
                        postion = i;
                        break;
                    }
                }

                if (postion > 0)
                {
                    target[postion] = 'R';
                }

                int start = postion + 1;
                for (int i = start; i < strLen; i++)
                {
                    target[i] = src[i];
                }
            });
        }

        public void CreateAction(Span<char> target, string src)
        {
            int postion = -1;
            for (int i = 0; i < src.Length; i++)
            {
                if (src[i] >= 'A' && src[i] <= 'Z')
                {
                    target[i] = src[i];
                }
                else
                {
                    postion = i;
                    break;
                }
            }

            if (postion > 0)
            {
                target[postion] = 'R';
            }

            int start = postion + 1;
            for (int i = start; i < src.Length; i++)
            {
                target[i] = src[i];
            }
        }

        //给Create所需的委托加缓存,而不是每次都创建新的委托实例
        public SpanAction<char, string> spanAction = null;
        public string CreateCache(string input)
        {
            int len = input.Length;

            if (spanAction == null) //只有为空的,才会创建委托实例
            {
                spanAction = CreateAction; //缓存委托实例
            }
            return string.Create(len, input, spanAction);
        }
    }
}

在使用静态委托,发现是不允许使用作用域外的变量,便调整了代码,增加一个不用static关键字修饰的测试示例,下面看一下性能基准测试结果:

使用静态委托和缓存委托那个性能更好呢?

由于基准测试,主要以.Net 5为基线和.Net 6进行对比,结果发现:

  1. 性能最好的还是缓存委托
  2. 静态委托和不使用static关键字修饰(不使用作用域外的变量)的要稍慢一下,在反编译源码发现是一样的,为什么静态委托要稍慢一点呢? 有点不太理解.
接着我们继续看一下LocalCreate反编译出来的代码:

public string LocalCreate(string input)
{
    int num2 = input.get_Length();
    string str = input;
    SpanAction<char, string> u003cu003e9_120 = StringCreateTest.u003cu003ec.u003cu003e9__12_0;
    if (u003cu003e9_120 == null)  //看到这里是不是感觉熟悉的套路,只创建一次委托示例对象,和缓存委托是一样,只是由编译器去做
    {
        u003cu003e9_120 = new SpanAction<char, string>(StringCreateTest.u003cu003ec.u003cu003e9, (Span<char> target, string src) => {
            int num = -1;
            int length = target.get_Length();
            int num1 = 0;
            while (num1 < length)
            {
                if (src[num1] < 'A' || src[num1] > 'Z')
                {
                    num = num1;
                    break;
                }
                else
                {
                    *((target.get_Item(num1))) = src[num1];
                    num1++;
                }
            }
            if (num > 0)
            {
                *((target.get_Item(num))) = 'R';
            }
            for (int i = num + 1; i < length; i++)
            {
                *((target.get_Item(i))) = src[i];
            }
        });
        StringCreateTest.u003cu003ec.u003cu003e9__12_0 = u003cu003e9_120;
    }
    return string.Create<string>(num2, str, u003cu003e9_120);
}

不使用static关键字修饰,在编译时,由编译器自动优化,性能基准测试是随着.Net版本更新,有所不同.

秋风 2021-10-05