編寫目的
在頻繁的字符串拼接中,為了提升程序的性能,我們往往會用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; }
百萬次追加 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.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; }
百萬次追加 '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; }
嗯..這個和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中試試
//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); } } } }
這樣就OK了,在很多時間他就是一個完全可以滿足需求的精簡提速版的StringBuilder!
包括以后如果機會放出個人用的簡易ORM也會發現里面的字符串拼接也是用的這個對象