C#效率极子 - 精益求精
自动化+性能优化

字符串连接,常用的三种方式:StringBuilder、+、string.Format。很多人认为StringBuilder的效率高于+,这种观念是不正确的。
一般来说,对于数量固定的字符串连接,+的效率是最高的。比如:

string sql = "update tableName set int1=" + int1.ToString() + ",int2=" + int2.ToString() + ",int3=" + int3.ToString() + " where id=" + id.ToString();

编译器会优化为

string sql = string.Concat(new string[] { "update tableName set int1=", int1.ToString(), ",int2=", int2.ToString(), ",int3=", int3.ToString(), " where id=", id.ToString() });

再来看看string.Concat是怎么实现的

string.Concat
public static string Concat(params string[] values)
{
    int totalLength = 0;
    if (values == null)
    {
        throw new ArgumentNullException("values");
    }
    string[] strArray = new string[values.Length];
    for (int i = 0; i < values.Length; i++)
    {
        string str = values[i];
        strArray[i] = (str == null) ? Empty : str;
        totalLength += strArray[i].Length;
        if (totalLength < 0)
        {
            throw new OutOfMemoryException();
        }
    }
    return ConcatArray(strArray, totalLength);
}
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;
}
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* chRef = &dest.m_firstChar)
    {
        fixed (char* chRef2 = &src.m_firstChar)
        {
            wstrcpy(chRef + destPos, chRef2, length);
        }
    }
}

看到没有,先计算目标字符串的长度,然后申请相应的空间,最后逐一复制。相当简练,基本上可以说没有多余操作,时间复杂度仅为o(n),而且常数项为1。总结:固定数量的字符串连接效率最高的是string.Concat,而连+被编译器优化为string.Concat,所以+的效率也是最高的。
注意:字符串的连+不要拆成多条语句,比如:

string sql = "update tableName set int1=";
sql += int1.ToString();
sql += ...

这样子的代码,编译器如果没有优化为string.Concat,也就变成了性能杀手,因为第i个字符串需要复制n-i次,时间复杂度就成了O(n^2)。

那如果字符串数量不固定怎么办呢?所以就有了StringBuilder,一般情况下它使用2n的空间来保证O(n)整体时间复杂度,常数项接近于2。

StringBuilder
public StringBuilder() : this(0x10)
{
}
public StringBuilder(int capacity) : this(string.Empty, capacity)
{
}
public StringBuilder(string value, int capacity) : this(value, 0, (value != null) ? value.Length : 0, capacity)
{
}
public StringBuilder(string value, int startIndex, int length, int capacity)
{
    this.m_currentThread = Thread.InternalGetCurrentThread();
    if (capacity < 0)
    {
        throw new ArgumentOutOfRangeException("capacity", string.Format(CultureInfo.CurrentCulture, Environment.GetResourceString("ArgumentOutOfRange_MustBePositive"), new object[] { "capacity" }));
    }
    if (length < 0)
    {
        throw new ArgumentOutOfRangeException("length", string.Format(CultureInfo.CurrentCulture, Environment.GetResourceString("ArgumentOutOfRange_MustBeNonNegNum"), new object[] { "length" }));
    }
    if (startIndex < 0)
    {
        throw new ArgumentOutOfRangeException("startIndex", Environment.GetResourceString("ArgumentOutOfRange_StartIndex"));
    }
    if (value == null)
    {
        value = string.Empty;
    }
    if (startIndex > (value.Length - length))
    {
        throw new ArgumentOutOfRangeException("length", Environment.GetResourceString("ArgumentOutOfRange_IndexLength"));
    }
    this.m_MaxCapacity = 0x7fffffff;
    if (capacity == 0)
    {
        capacity = 0x10;
    }
    while (capacity < length)
    {
        capacity *= 2;
        if (capacity < 0)
        {
            capacity = length;
            break;
        }
    }
    this.m_StringValue = string.GetStringForStringBuilder(value, startIndex, length, capacity);
}

我们看到,默认情况下,它初始化的时候申请一个长度为16的字符串作为容器。

StringBuilder.Append
public StringBuilder Append(string value)
{
    if (value != null)
    {
        string stringValue = this.m_StringValue;
        IntPtr currentThread = Thread.InternalGetCurrentThread();
        if (this.m_currentThread != currentThread)
        {
            stringValue = string.GetStringForStringBuilder(stringValue, stringValue.Capacity);
        }
        int length = stringValue.Length;
        int requiredLength = length + value.Length;
        if (this.NeedsAllocation(stringValue, requiredLength))
        {
            string newString = this.GetNewString(stringValue, requiredLength);
            newString.AppendInPlace(value, length);
            this.ReplaceString(currentThread, newString);
        }
        else
        {
            stringValue.AppendInPlace(value, length);
            this.ReplaceString(currentThread, stringValue);
        }
    }
    return this;
}
private string GetNewString(string currentString, int requiredLength)
{
    int maxCapacity = this.m_MaxCapacity;
    if (requiredLength < 0)
    {
        throw new OutOfMemoryException();
    }
    if (requiredLength > maxCapacity)
    {
        throw new ArgumentOutOfRangeException("requiredLength", Environment.GetResourceString("ArgumentOutOfRange_SmallCapacity"));
    }
    int capacity = currentString.Capacity * 2;
    if (capacity < requiredLength)
    {
        capacity = requiredLength;
    }
    if (capacity > maxCapacity)
    {
        capacity = maxCapacity;
    }
    if (capacity <= 0)
    {
        throw new ArgumentOutOfRangeException("newCapacity", Environment.GetResourceString("ArgumentOutOfRange_NegativeCapacity"));
    }
    return string.GetStringForStringBuilder(currentString, capacity);
}

当空间不够时,它重新申请至少两倍的空间以满足需求。我们可以看到,这种方式最坏的执行时间为n+n/2+n/4+n/8+...,接近于2n。因为这个算法的实用与高效,.net类库里面有很多动态集合都采用这种牺牲空间换取时间的方式,一般来说效果还是不错的。
但是StringBuilder相对于+=来说不够简洁,所以当n较小时一般使用O(n^2)的+=,当n稍大一点一般使用StringBuilder。


再来看看string.Format。有人说他比+效率高,因为它的底层是StringBuilder。很明显这种说法已经不攻自破了,抛开string.Format底层需要解析{?}的代价,StringBuilder自己就PK不过+,难道你是想PK+=,只能说你是故意的。


另外还有一种方式,那就是List<string>,我个人比较常用,也是我推荐使用的常用方式,因为它可以转换为string[]后使用string.Concat或string.Join,很多时候都比StringBuilder更高效。List与StringBuilder采用的是同样的动态集合算法,时间复杂度也是O(n),与StringBuilder不同的是:List的n是字符串的数量,复制的是字符串的引用;StringBuilder的n是字符串的长度,复制的数据。不同的特性决定的它们各自的适应环境,当子串比较大时建议使用List<string>,因为复制引用比复制数据划算。而当子串比较小,比如平均长度小于8,特别是一个一个的字符,建议使用StringBuilder。

posted on 2012-05-09 12:50  肖进  阅读(1114)  评论(3编辑  收藏  举报