zoukankan      html  css  js  c++  java
  • 从.Net版本演变看String和StringBuilder性能之争

    在C#中string关键字的映射实际上指向.NET基类System.String。System.String是一个功能非常强大且用途非常广泛的基类,所以我们在用C#string的时候实际就是在用.NET Framework String。String上是一个不可变的数据类型,一旦对字符串对象进行了初始化,该字符串对象就不能改变了。表面上修改字符串内容的方法和运算符实际上创建一个新字符串,所以重复修改给定的字符串,效率会很低。所以.Net Framework定义了另一个StringBuild类以提高字符串处理的性能,但String和StringBuild之间又有什么联系呢。

     以下一个示例基于版本.Net Framework2.0这个示例主要是参考重谈字符串性能,先定义一个简单性能计数器主要目的有:

    (1)打印出各字符串处理方法的消耗时间

    (2)CPU时钟周期

    (3)执行过程中垃圾回收器回收次数

     public class CodeTimer
        {
            public delegate void Action();
            /// <summary>
            /// 初始化
            /// </summary>
            public static void Initialize()
            {
                Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
                Thread.CurrentThread.Priority = ThreadPriority.Highest;
                Time("", 1, () => { });
            }
            public static void Time(string name, int iteration, Action action)
            {
                if (String.IsNullOrEmpty(name)) return;
    
                // 1.
                ConsoleColor currentForeColor = Console.ForegroundColor;
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine(name);
    
                // 2.
                GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
                int[] gcCounts = new int[GC.MaxGeneration + 1];
                for (int i = 0; i <= GC.MaxGeneration; i++)
                {
                    gcCounts[i] = GC.CollectionCount(i);
                }
    
                // 3.
                Stopwatch watch = new Stopwatch();
                watch.Start();
                ulong cycleCount = GetCycleCount();
                for (int i = 0; i < iteration; i++) action();
                ulong cpuCycles = GetCycleCount() - cycleCount;
                watch.Stop();
    
                // 4.
                Console.ForegroundColor = currentForeColor;
                Console.WriteLine("	Time Elapsed:	" + watch.ElapsedMilliseconds.ToString("N0") + "ms");
                Console.WriteLine("	CPU Cycles:	" + cpuCycles.ToString("N0"));
    
                // 5.
                for (int i = 0; i <= GC.MaxGeneration; i++)
                {
                    int count = GC.CollectionCount(i) - gcCounts[i];
                    Console.WriteLine("	Gen " + i + ": 		" + count);
                }
    
                Console.WriteLine();
            }
    
            private static ulong GetCycleCount()
            {
                ulong cycleCount = 0;
                QueryThreadCycleTime(GetCurrentThread(), ref cycleCount);
                return cycleCount;
            }
    
            [DllImport("kernel32.dll")]
            [return: MarshalAs(UnmanagedType.Bool)]
            static extern bool QueryThreadCycleTime(IntPtr threadHandle, ref ulong cycleTime);
    
            [DllImport("kernel32.dll")]
            static extern IntPtr GetCurrentThread();
        }

    原文链接:一个简单的性能计数器:CodeTimer

    定义一个StringListBuilder用List<string>先将所有字符串保存起来最后转化为字符串数组,再返回字符串

    public class StringListBuilder
        {
            private List<string> m_list = new List<string>();
    
            public StringListBuilder Append(string s)
            {
                this.m_list.Add(s);
                return this;
            }
    
            public override string ToString()
            {
                return String.Concat(this.m_list.ToArray());
            }
        }

    定义一个StrPerformance类用于维护各个字符串处理的方法

     public class StrPerformance
        {
            private static readonly string STR = "0123456789";
    
            public static string NormalConcat(int count)
            {
                var result = "";
                for (int i = 0; i < count; i++) result += STR;
                return result;
            }
    
            public static string StringBuilder(int count)
            {
                var builder = new StringBuilder();
                for (int i = 0; i < count; i++) builder.Append(STR);
                return builder.ToString();
            }
    
            public static string StringListBuilder(int count)
            {
                var builder = new StringListBuilder();
                for (int i = 0; i < count; i++) builder.Append(STR);
                return builder.ToString();
            }
    
            public static string StringConcat(int count)
            {
                var array = new string[count];
                for (int i = 0; i < count; i++) array[i] = STR;
                return String.Concat(array);
            }
        }
    View Code

    用性能计数器记录各个方法执行过程并且打印出对应的参数

    CodeTimer.Initialize();
    
                for (int i = 2; i <= 2048; i *= 2)
                {
                    CodeTimer.Time(
                    String.Format("StringListBuilder ({0})", i),
                    10000,
                    () => StrPerformance.StringListBuilder(i));
    
                    CodeTimer.Time(
                      String.Format("String concat ({0})", i),
                      10000,
                      () => StrPerformance.StringConcat(i));
                    CodeTimer.Time(
                        String.Format("StringBuilder ({0})", i),
                        10000,
                        () => StrPerformance.StringBuilder(i));
    
                }

    分析可以得出,广受追捧的StringBuilder性能似乎并不是最好的,String.Concat方法有时候有时候更适合使用。那么为什么String.Concat方法性能那么高,StringBuilder反而比StringListBuilder要差,要知道StringListBuilder还要维护一个集合,通过反编译我们看一下.NET2.0的String.Concat和StringBuilder到底是怎么实现的。

    先看在.Net2.0下StringBuilder的Append和ToString方法的实现过程,Append和ToString实现过程。

    // System.Text.StringBuilder
    public StringBuilder Append(string value)
    {
        if (value == null)
        {
            return this;
        }
        string text = this.m_StringValue;
        IntPtr intPtr = Thread.InternalGetCurrentThread();
        if (this.m_currentThread != intPtr)
        {
            text = string.GetStringForStringBuilder(text, text.Capacity);
        }
        int length = text.Length;
        int requiredLength = length + value.Length;
        if (this.NeedsAllocation(text, requiredLength))
        {
            string newString = this.GetNewString(text, requiredLength);
            newString.AppendInPlace(value, length);
            this.ReplaceString(intPtr, newString);
        }
        else
        {
            text.AppendInPlace(value, length);
            this.ReplaceString(intPtr, text);
        }
        return this;
    }
    View Code
    public override string ToString()
    {
        string currentValue = this.m_currentValue;
    
        if (this.m_currentThread != Thread.InternalGetCurrentThread())
        {
            return string.InternalCopy(currentValue);
        }
    
        // 如果这个字符串对象“太空”的话
        if ((2 * currentValue.Length) < currentValue.ArrayLength)
        {
            // 则构造一个“满当”地对象
            return string.InternalCopy(currentValue);
        }
    
        // 将字符序列最后放一个
        currentValue.ClearPostNullChar();
    
        // 既然容器已经“暴露”,则设制“当前线程”的标识为Zero,
        // 这意味着下次操作会生成新字符串对象(即新的容器)
        this.m_currentThread = IntPtr.Zero;
    
        // 如果“还不算太空”,则返回当前对象
        return currentValue;
    }
    View Code

    StringBuilder的ToString方法比较有意思,它会判断到底是“构造一个新对象”还是就“直接返回当前容器”给你。如果直接返回当前容器,则可能会浪费较多内存,而如果构造一个新对象,则又会损耗性能。让StringBuilder做出决定的便是容器内部的字符序列占“最大容积”的比例,如果超过一半,则表明“还不算太空”,便选择“时间”,直接返回容器;否则,StringBuilder会认为还是选择“空间”较为合算,便构造一个新对象并返回,至于当前的容器便会和StringBuilder一道被GC回收了。

    同时我们可以看到,如果返回了新对象,则当前容器还可以继续在Append时使用,否则Append方法便会因为m_currentValue为Zero而创建新的容器。不过,从ToString的实现中也可以看出,多次调用ToString方法一定返回新建的对象。

    而String.Concat又做了什么,String类Concat的具体实现过程

    public static string Concat(params string[] values)
    {
        int totalLength = 0;
    
        if (values == null)
        {
            throw new ArgumentNullException("values");
        }
    
        string[] arrayToConcate = new string[values.Length];
    
        // 遍历源数组,填充拼接用的数组
        for (int i = 0; i < values.Length; i++)
        {
            string str = values[i];
    
            // null作为空字符串对待
            arrayToConcate[i] = (str == null) ? Empty : str;
    
            // 累计字符串总长度
            totalLength += arrayToConcate[i].Length;
    
            // 如果越界了,抛异常
            if (totalLength < 0)
            {
                throw new OutOfMemoryException();
            }
        }
    
        // 拼接
        return ConcatArray(arrayToConcate, totalLength);
    }
    View Code
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern string FastAllocateString(int length);
    
    private static string ConcatArray(string[] values, int totalLength)
    {
        // 分配目标字符串所占用的空间(即创建对象)
        string dest = FastAllocateString(totalLength);
    
        int destPos = 0;
    
        for (int i = 0; i < values.Length; i++)
        {
            // 不断将源字符串的每个元素填充至目标位置
            FillStringChecked(dest, destPos, values[i]);
    
            // 偏移量不断更新
            destPos += values[i].Length;
        }
    
        return dest;
    }
    View Code
    private static unsafe void FillStringChecked(string dest, int destPos, string src)
    {
        int length = src.Length;
        if (length > (dest.Length - destPos))
        {
            throw new IndexOutOfRangeException();
        }
    
        fixed (char* chDest = &dest.m_firstChar)
        {
            fixed (char* chSrc = &src.m_firstChar)
            {
                wstrcpy(chDest + destPos, chSrc, length);
            }
        }
    }
    View Code

    由于数组中的字符串都是确定的因此事先计算出结果的长度,于是遍历源字符串数组,将它们一个一个复制(或叫做“填充”)到目标字符串的某一段位置上去,因为在此之前已经确定结果的大小,因此直接创建一个“容器”即可,剩下的只是填充数据而已。既然可以不浪费任何一寸空间,也没有任何多余的操作,这也是String.Concat高效的原因。

    同样的代码移植到.Net 4.5上会不会还像之前一样String.Concat在处理连接字符串中性能最高

    这次StringBuilder又重新回到了我们最初的印象中,在处理多字符串连接的时候StringBuilder是性能最高的,通过和.Net 2.0的实验结果来看StringListBuilder和String Concat的性能变化不大,而似乎StringBuilder的性能提高了一倍,那么在.NET 4.5中StringBuilder的Append方法又做了什么呢,下面我们来看一下.Net 4.5中Append的具体实现过程

     public unsafe StringBuilder Append(string value)
            {
                if (value != null)
                {
                    //StringBuilder内维护的一个字符数组
                    char[] chunkChars = this.m_ChunkChars;
                    int chunkLength = this.m_ChunkLength;
                    int length = value.Length;
                    int num = chunkLength + length;
    
                    //不必增加m_ChunkChars字符数组的长度
                    if (num < chunkChars.Length)
                    {
                        if (length <= 2)
                        {
                            if (length > 0)
                            {
                                chunkChars[chunkLength] = value[0];
                            }
                            if (length > 1)
                            {
                                chunkChars[chunkLength + 1] = value[1];
                            }
                        }
                        else
                        {
                            fixed (string text = value)
                            {
                                char* ptr = text;
                                if (ptr != null)
                                {
                                    ptr += RuntimeHelpers.OffsetToStringData / 2;
                                }
                                fixed (char* ptr2 = &chunkChars[chunkLength])
                                {
                                    string.wstrcpy(ptr2, ptr, length);
                                }
                            }
                        }
                        this.m_ChunkLength = num;
                    }
                    //增加m_ChunkChars数组的长度
                    else
                    {
                        this.AppendHelper(value);
                    }
                }
                return this;
            }
     private unsafe void AppendHelper(string value)
            {
                fixed (string text = value)
                {
                    //去字符串的地址
                    char* ptr = text;
                    if (ptr != null)
                    {
                        ptr += RuntimeHelpers.OffsetToStringData / 2;
                    }
                    this.Append(ptr, value.Length);
                }
            }
    
            public unsafe StringBuilder Append(char* value, int valueCount)
            {
                if (valueCount < 0)
                {
                    throw new ArgumentOutOfRangeException("valueCount", Environment.GetResourceString("ArgumentOutOfRange_NegativeCount"));
                }
                int num = valueCount + this.m_ChunkLength;
                if (num <= this.m_ChunkChars.Length)
                {
                    //把字符串一个一个复制到m_ChunkChars字符数组中
                    StringBuilder.ThreadSafeCopy(value, this.m_ChunkChars, this.m_ChunkLength, valueCount);
                    this.m_ChunkLength = num;
                }
                else
                {
                    int num2 = this.m_ChunkChars.Length - this.m_ChunkLength;
                    if (num2 > 0)
                    {
                        StringBuilder.ThreadSafeCopy(value, this.m_ChunkChars, this.m_ChunkLength, num2);
                        this.m_ChunkLength = this.m_ChunkChars.Length;
                    }
                    int num3 = valueCount - num2;
                    this.ExpandByABlock(num3);
                    StringBuilder.ThreadSafeCopy(value + num2, this.m_ChunkChars, 0, num3);
                    this.m_ChunkLength = num3;
                }
                return this;
            }
    View Code

    在分析代码可知在.Net 4.5StringBuilder中内部维护了一个m_ChunkChars字符数组,来避免不断扩容,不断复制的过程所造成的性能消耗,所以StringBuilder性能又成为三者中最高的一个。

    看了老赵blog之后,(此处省去一千溢美之词)——只想说一句:“我对阁下的景仰有如滔滔江水,连绵不绝,又如黄河泛滥,一发而不可收拾!

  • 相关阅读:
    页眉插入图片,文字和页号(码)的设置
    MIT_JOS_Lab5
    MIT_JOS_Lab4_PartB_and_PartC
    MIT_JOS_Lab4_PartA
    Monte Carlo Integration
    A strategy to quantify embedding layer
    From DFA to KMP algorithm
    A problem of dimension in Vector Space and It's nullspace
    Pytorch 模型的存储与加载
    Jensen's inequality 及其应用
  • 原文地址:https://www.cnblogs.com/simen-tan/p/6864069.html
Copyright © 2011-2022 走看看