精簡版StringBuilder,提速字符串拼接


  編寫目的

在頻繁的字符串拼接中,為了提升程序的性能,我們往往會用StringBuilder代替String+=String這樣的操作;

而我在實際編碼中發現,大部分情況下我用到的只是StringBuilder的Append方法;

一些極端的情況下,我希望我的程序性能更高,這時從StringBuilder入手是一個不錯的主意;

所以我希望用一種簡單的方案代替StringBuilder,我將這個方案命名為QuickStringWriter;

 

  方案定義

對於StringBuilder來說他除了Append之外還會有更多的方法,比如Insert,AppendFormat等

QuickStringWriter這個方案,僅僅是用來代替簡單的字符串+=這樣的操作,所以我不會考慮他們,只需要重新實現Append,並讓他們比StringBuilder更快

 

  初步設計

class QuickStringWriter : IDisposable
{
    public QuickStringWriter Append(bool val);
    public QuickStringWriter Append(byte val);
    public QuickStringWriter Append(char val);
    public QuickStringWriter Append(DateTime val);
    public QuickStringWriter Append(DateTime val, string format);
    public QuickStringWriter Append(decimal val);
    public QuickStringWriter Append(double val);
    public QuickStringWriter Append(Guid val);
    public QuickStringWriter Append(Guid val, string format);
    public QuickStringWriter Append(short val);
    public QuickStringWriter Append(int val);
    public QuickStringWriter Append(long val);
    public QuickStringWriter Append(sbyte val);
    public QuickStringWriter Append(float val);
    public QuickStringWriter Append(string val);
    public QuickStringWriter Append(ushort val);
    public QuickStringWriter Append(uint val);
    public QuickStringWriter Append(ulong val);
    public QuickStringWriter Clear();
    void Dispose();
    string ToString();
}

  結構

QuickStringWriter將使用一個Char數組作為緩沖區(Buff)

使用一個屬性Position作為當前字符位置,或者說是當前字符數

重寫ToString方法,將當前緩沖區中的內容,從0到Position轉為string對象輸出

char[] Buff;
int Position;

public override string ToString()
{
    return new string(Buff, 0, Position);
}

  設置緩沖區

既然有緩沖區,那么就要考慮緩沖區不足時的處理

我設計2個方法解決這個問題

//設置緩沖區容量
void SetCapacity(int capacity)
{
    if (capacity > Buff.Length)
    {
        if (capacity > 6000 * 10000)   //6000W
        {
            throw new OutOfMemoryException("QuickStringWriter容量不能超過6000萬個字符");
        }
    }
    var newbuff = new char[capacity];
    Array.Copy(Buff, 0, newbuff, 0, Math.Min(Position, capacity));
    Buff = newbuff;
    Position = Math.Min(Position, Buff.Length);
}
//翻倍空間
void ToDouble()
{
    SetCapacity(Math.Min(Buff.Length * 2, 10 * 10000));
}

 

第一個方法SetCapacity,我預留了一個縮小當前緩沖區的處理,雖然現在不會使用

第二個方法就是翻倍緩沖區,這里也是有個條件的,如果當前緩沖區大於5W,最多一次也只能擴容10W字符的容量

//當容量不足的時候,嘗試翻倍空間
void Try()
{
    if (Position >= Buff.Length)
    {
        ToDouble();
    }
}
//測試剩余空間大小,如果不足,則擴展至可用大小后再翻倍
void Check(int count)
{
    var pre = Position + count;
    if (pre >= Buff.Length)
    {
        SetCapacity(pre * 2);
    }
}

 

這里還需要2個方法可以方面的調用 

比如在追加單個字符的時候可以調用Try

在追加指定長度字符之前可以調用Check

 

  性能

在性能上,我只要考慮每一個方法的性能都能快StringBuilder就可以了,這點其實並不是非常困難

public QuickStringWriter Append(Boolean val)
{
    if (val)
    {
        Check(4);
        Buff[Position++] = 't';
        Buff[Position++] = 'r';
        Buff[Position++] = 'u';
        Buff[Position++] = 'e';
    }
    else
    {
        Check(5);
        Buff[Position++] = 'f';
        Buff[Position++] = 'a';
        Buff[Position++] = 'l';
        Buff[Position++] = 's';
        Buff[Position++] = 'e';
    }
    return this;
}
bool類型處理

百萬次追加 false

StringBuilder         19ms

QuickStringWriter  9ms

ps:系統的bool轉換為String后首字母都是大小,這里我為了使用更方面直接轉為小寫的了

public QuickStringWriter Append(DateTime val)
{
    Check(18);
    if (val.Year < 1000)
    {
        Buff[Position++] = '0';
        if (val.Year < 100)
        {
            Buff[Position++] = '0';
            if (val.Year < 10)
            {
                Buff[Position++] = '0';
            }
        }
    }
    Append((long)val.Year);
    Buff[Position++] = '-';

    if (val.Month < 10)
    {
        Buff[Position++] = '0';
    }
    Append((long)val.Month);
    Buff[Position++] = '-';

    if (val.Day < 10)
    {
        Buff[Position++] = '0';
    }
    Append((long)val.Day);
    Buff[Position++] = ' ';

    if (val.Hour < 10)
    {
        Buff[Position++] = '0';
    }
    Append((long)val.Hour);
    Buff[Position++] = ':';

    if (val.Minute < 10)
    {
        Buff[Position++] = '0';
    }
    Append((long)val.Minute);
    Buff[Position++] = ':';

    if (val.Second < 10)
    {
        Buff[Position++] = '0';
    }
    Append((long)val.Minute);
    return this;
}
DateTime類型處理

十萬次追加 DateTime.Now

StringBuilder         90ms

QuickStringWriter  55ms

Char[] NumberBuff;
public QuickStringWriter Append(Int64 val)
{
    if (val == 0)
    {
        Buff[Position++] = '0';
        return this;
    }

    var pos = 63;
    if (val < 0)
    {
        Buff[Position++] = '-';
        NumberBuff[pos] = (char)(~(val % 10) + '1');
        if (val < -10)
        {
            val = val / -10;
            NumberBuff[--pos] = (char)(val % 10 + '0');
        }
    }
    else
    {
        NumberBuff[pos] = (char)(val % 10 + '0');
    }
    while ((val = val / 10L) != 0L)
    {
        NumberBuff[--pos] = (char)(val % 10L + '0');
    }
    var length = 64 - pos;
    Check(length);
    Array.Copy(NumberBuff, pos, Buff, Position, length);
    Position += length;
    return this;
}
整數類型的處理

百萬次追加             long.MaxValue    sbyte.MaxValue

StringBuilder         190ms                 120ms  

QuickStringWriter  115ms                 33ms

public QuickStringWriter Append(Char val)
{
    Try();
    Buff[Position++] = val;
    return this;
}
char類型處理

百萬次追加             'a'

StringBuilder         7ms

QuickStringWriter  4ms

public QuickStringWriter Append(String val)
{
    if (val == null || val.Length == 0)
    {
        return this;
    }
    else if (val.Length <= 3)
    {
        Check(val.Length);
        Buff[Position++] = val[0];
        if (val.Length > 1)
        {
            Buff[Position++] = val[1];
            if (val.Length > 2)
            {
                Buff[Position++] = val[2];
            }
        }
    }
    else
    {
        Check(val.Length);
        val.CopyTo(0, Buff, Position, val.Length);
        Position += val.Length;
    }
    return this;
}
String處理

嗯..這個和StringBuilder幾乎相同

然后其他的類型就直接按照調用Append(string str) 的處理方式就可以了

public QuickStringWriter Append(Guid val)
{
    Append(val.ToString());
    return this;
}

public QuickStringWriter Append(Decimal val)
{
    Append(val.ToString(System.Globalization.NumberFormatInfo.InvariantInfo));
    return this;
}
其他類型處理

  代入JsonBuilder

 全部完成了之后 我把他加入到之前的JsonBuilder中試試

//protected StringBuilder Buff = new StringBuilder(4096);
protected QuickStringWriter Buff = new QuickStringWriter(4096);//字符緩沖區

調用需要修改一個地方

public string ToJsonString(object obj)
{
    //Buff.Length = 0; //StringBuilder清空方法
    Buff.Clear();//QuickStringWriter清空方法
    AppendObject(obj);
    return Buff.ToString();
}

再來看看前后的差距

  • 原StringBuilder緩沖

211ms | 166ms | 162ms | 164ms | 164ms | 160ms | 163ms | 160ms | 179ms | 156ms |

  • 更換為QuickStringWriter后

167ms | 134ms | 134ms | 134ms | 135ms | 134ms | 134ms | 133ms | 134ms | 134ms |

ps:在公司的破電腦上也測了,發現配置越差差距越大,在公司的酷睿雙核上差距應該是40%左右

  完整代碼

using System;
using System.Collections.Generic;
using System.Text;

namespace blqw
{
    public class QuickStringWriter : IDisposable
    {
        public QuickStringWriter() : this(2048) { }
        /// <summary>
        /// 實例化新的對象,並且指定初始容量
        /// </summary>
        /// <param name="capacity"></param>
        public QuickStringWriter(int capacity)
        {
            NumberBuff = new Char[64];
            Buff = new Char[capacity];
        }

        //設置緩沖區容量
        void SetCapacity(int capacity)
        {
            if (capacity > Buff.Length)
            {
                if (capacity > 6000 * 10000)   //6000W
                {
                    throw new OutOfMemoryException("QuickStringWriter容量不能超過6000萬個字符");
                }
            }
            var newbuff = new char[capacity];
            Array.Copy(Buff, 0, newbuff, 0, Math.Min(Position, capacity));
            Buff = newbuff;
            Position = Math.Min(Position, Buff.Length);
        }
        //當容量不足的時候,嘗試翻倍空間
        void ToDouble()
        {
            SetCapacity(Math.Min(Buff.Length * 2, 10 * 10000));
        }

        Char[] NumberBuff;
        Char[] Buff;
        int Position;

        public void Dispose()
        {
            NumberBuff = null;
            Buff = null;
        }

        public QuickStringWriter Append(Boolean val)
        {
            if (val)
            {
                Check(4);
                Buff[Position++] = 't';
                Buff[Position++] = 'r';
                Buff[Position++] = 'u';
                Buff[Position++] = 'e';
            }
            else
            {
                Check(5);
                Buff[Position++] = 'f';
                Buff[Position++] = 'a';
                Buff[Position++] = 'l';
                Buff[Position++] = 's';
                Buff[Position++] = 'e';
            }
            return this;
        }
        public QuickStringWriter Append(DateTime val)
        {
            Check(18);
            if (val.Year < 1000)
            {
                Buff[Position++] = '0';
                if (val.Year < 100)
                {
                    Buff[Position++] = '0';
                    if (val.Year < 10)
                    {
                        Buff[Position++] = '0';
                    }
                }
            }
            Append((long)val.Year);
            Buff[Position++] = '-';

            if (val.Month < 10)
            {
                Buff[Position++] = '0';
            }
            Append((long)val.Month);
            Buff[Position++] = '-';

            if (val.Day < 10)
            {
                Buff[Position++] = '0';
            }
            Append((long)val.Day);
            Buff[Position++] = ' ';

            if (val.Hour < 10)
            {
                Buff[Position++] = '0';
            }
            Append((long)val.Hour);
            Buff[Position++] = ':';

            if (val.Minute < 10)
            {
                Buff[Position++] = '0';
            }
            Append((long)val.Minute);
            Buff[Position++] = ':';

            if (val.Second < 10)
            {
                Buff[Position++] = '0';
            }
            Append((long)val.Minute);
            return this;
        }

        public QuickStringWriter Append(Guid val)
        {
            Append(val.ToString());
            return this;
        }

        public QuickStringWriter Append(DateTime val, string format)
        {

            Append(val.ToString(format));
            return this;
        }
        public QuickStringWriter Append(Guid val, string format)
        {
            Append(val.ToString(format));
            return this;
        }

        public QuickStringWriter Append(Decimal val)
        {
            Append(val.ToString());
            return this;
        }
        public QuickStringWriter Append(Double val)
        {
            Append(Convert.ToString(val));
            return this;
        }
        public QuickStringWriter Append(Single val)
        {
            Append(Convert.ToString(val));
            return this;
        }


        public QuickStringWriter Append(SByte val)
        {
            Append((Int64)val);
            return this;
        }
        public QuickStringWriter Append(Int16 val)
        {
            Append((Int64)val);
            return this;
        }
        public QuickStringWriter Append(Int32 val)
        {
            Append((Int64)val);
            return this;
        }

        public override string ToString()
        {
            return new string(Buff, 0, Position);
        }

        public QuickStringWriter Append(Int64 val)
        {
            if (val == 0)
            {
                Buff[Position++] = '0';
                return this;
            }

            var pos = 63;
            if (val < 0)
            {
                Buff[Position++] = '-';
                NumberBuff[pos] = (char)(~(val % 10) + '1');
                if (val < -10)
                {
                    val = val / -10;
                    NumberBuff[--pos] = (char)(val % 10 + '0');
                }
            }
            else
            {
                NumberBuff[pos] = (char)(val % 10 + '0');
            }
            while ((val = val / 10L) != 0L)
            {
                NumberBuff[--pos] = (char)(val % 10L + '0');
            }
            var length = 64 - pos;
            Check(length);
            Array.Copy(NumberBuff, pos, Buff, Position, length);
            Position += length;
            return this;
        }
        public QuickStringWriter Append(Char val)
        {
            Try();
            Buff[Position++] = val;
            return this;
        }
        public QuickStringWriter Append(String val)
        {
            if (val == null || val.Length == 0)
            {
                return this;
            }
            else if (val.Length <= 3)
            {
                Check(val.Length);
                Buff[Position++] = val[0];
                if (val.Length > 1)
                {
                    Buff[Position++] = val[1];
                    if (val.Length > 2)
                    {
                        Buff[Position++] = val[2];
                    }
                }
            }
            else
            {
                Check(val.Length);
                val.CopyTo(0, Buff, Position, val.Length);
                Position += val.Length;
            }
            return this;
        }



        public QuickStringWriter Append(Byte val)
        {
            Append((UInt64)val);
            return this;
        }
        public QuickStringWriter Append(UInt16 val)
        {
            Append((UInt64)val);
            return this;
        }
        public QuickStringWriter Append(UInt32 val)
        {
            Append((UInt64)val);
            return this;
        }
        public QuickStringWriter Append(UInt64 val)
        {
            if (val == 0)
            {
                Buff[Position++] = '0';
                return this;
            }
            var pos = 63;

            NumberBuff[pos] = (char)(val % 10 + '0');

            while ((val = val / 10L) != 0L)
            {
                NumberBuff[--pos] = (char)(val % 10L + '0');
            }
            var length = 64 - pos;
            Check(length);
            Array.Copy(NumberBuff, pos, Buff, Position, length);
            Position += length;
            return this;
        }

        public QuickStringWriter Clear()
        {
            Position = 0;
            return this;
        }

        //當容量不足的時候,嘗試翻倍空間
        void Try()
        {
            if (Position >= Buff.Length)
            {
                ToDouble();
            }
        }
        //測試剩余空間大小,如果不足,則擴展至可用大小后再翻倍
        void Check(int count)
        {
            var pre = Position + count;
            if (pre >= Buff.Length)
            {
                SetCapacity(pre * 2);
            }
        }


    }
}
QuickStringWriter完整代碼

這樣就OK了,在很多時間他就是一個完全可以滿足需求的精簡提速版的StringBuilder!

包括以后如果機會放出個人用的簡易ORM也會發現里面的字符串拼接也是用的這個對象

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM