使用前缀,后缀和分隔符连接字符串的最快方法

时间:2012-11-16 15:30:22

标签: c# .net string

更新

关注Mr Cheese's answer,似乎是

public static string Join<T>(string separator, IEnumerable<T> values)

string.Join的重载通过使用StringBuilderCache类来获得优势。

是否有人对此声明的正确性或原因有任何反馈意见?

我能写自己的吗,

public static string Join<T>(
    string separator,
    string prefix,
    string suffix,
    IEnumerable<T> values)

使用StringBuilderCache类的函数?


提交my answer to this question后,我得到了一些分析,其中表现最佳。

我在控制台Program课程中编写了这段代码来测试我的想法。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;

class Program
{
    static void Main()
    {
        const string delimiter = ",";
        const string prefix = "[";
        const string suffix = "]";
        const int iterations = 1000000;

        var sequence = Enumerable.Range(1, 10).ToList();

        Func<IEnumerable<int>, string, string, string, string>[] joiners =
            {
                Build,
                JoinFormat,
                JoinConcat
            };

        // Warmup
        foreach (var j in joiners)
        {
            Measure(j, sequence, delimiter, prefix, suffix, 5);
        }

        // Check
        foreach (var j in joiners)
        {
            Console.WriteLine(
                "{0} output:\"{1}\"",
                j.Method.Name,
                j(sequence, delimiter, prefix, suffix));
        }

        foreach (var result in joiners.Select(j => new
                {
                    j.Method.Name,
                    Ms = Measure(
                        j,
                        sequence,
                        delimiter,
                        prefix,
                        suffix,
                        iterations)
                }))
        {
            Console.WriteLine("{0} time = {1}ms", result.Name, result.Ms);
        }

        Console.ReadKey();
    }

    private static long Measure<T>(
        Func<IEnumerable<T>, string, string, string, string> func,
        ICollection<T> source,
        string delimiter,
        string prefix,
        string suffix,
        int iterations)
    {
        var stopwatch = new Stopwatch();

        stopwatch.Start();
        for (var i = 0; i < iterations; i++)
        {
            func(source, delimiter, prefix, suffix);
        }

        stopwatch.Stop();

        return stopwatch.ElapsedMilliseconds;
    }

    private static string JoinFormat<T>(
        IEnumerable<T> source,
        string delimiter,
        string prefix,
        string suffix)
    {
        return string.Format(
            "{0}{1}{2}",
            prefix,
            string.Join(delimiter, source),
            suffix);
    }

    private static string JoinConcat<T>(
        IEnumerable<T> source,
        string delimiter,
        string prefix,
        string suffix)
    {
        return string.Concat(
            prefix,
            string.Join(delimiter, source),
            suffix);
    }

    private static string Build<T>(
        IEnumerable<T> source,
        string delimiter,
        string prefix,
        string suffix)
    {
        var builder = new StringBuilder();
        builder = builder.Append(prefix);

        using (var e = source.GetEnumerator())
        {
            if (e.MoveNext())
            {
                builder.Append(e.Current);
            }

            while (e.MoveNext())
            {
                builder.Append(delimiter);
                builder.Append(e.Current);
            }
        }

        builder.Append(suffix);
        return builder.ToString();
    }
}

运行代码,在发布配置中,使用优化构建,从命令行我得到这样的输出。

  

...

     

构建时间= 1555ms

     

JoinFormat时间= 1715ms

     

JoinConcat时间= 1452ms

这里唯一的惊喜(对我而言)是Join-Format组合是最慢的。在考虑this answer后,这会更有意义,string.Join的输出正由StringBuilder中的外string.Format处理,这种方法存在固有的延迟

在沉思之后,我不清楚string.Join如何更快。我已经阅读了它对FastAllocateString()的使用,但我不明白如何在.ToString()的每个成员上调用sequence时如何准确预先分配缓冲区。为什么Join-Concat组合更快?

一旦我理解了这一点,是否可以编写我自己的unsafe string Join函数,该函数需要额外的prefixsuffix参数,然后执行“安全”替代。

我有过几次尝试,虽然它们有效,但它们并不快。

4 个答案:

答案 0 :(得分:4)

为了尝试回答您的原始问题,我认为答案在于(神奇的)Reflector工具。您正在使用IEnumerable对象的集合,这也会导致调用String.Join方法中相同类型的重载。有趣的是,这个函数与你的Build函数非常相似,因为它枚举了集合并使用了字符串构建器,这意味着它不需要事先知道所有字符串的长度。

public static string Join<T>(string separator, IEnumerable<T> values)
{

    if (values == null)
    {
        throw new ArgumentNullException("values");
    }
    if (separator == null)
    {
        separator = Empty;
    }
    using (IEnumerator<T> enumerator = values.GetEnumerator())
    {
        if (!enumerator.MoveNext())
        {
            return Empty;
        }
        StringBuilder sb = StringBuilderCache.Acquire(0x10);
        if (enumerator.Current != null)
        {
            string str = enumerator.Current.ToString();
            if (str != null)
            {
                sb.Append(str);
            }
        }
        while (enumerator.MoveNext())
        {
            sb.Append(separator);
            if (enumerator.Current != null)
            {
                string str2 = enumerator.Current.ToString();
                if (str2 != null)
                {
                    sb.Append(str2);
                }
            }
        }
        return StringBuilderCache.GetStringAndRelease(sb);
    }
}

似乎是在使用缓存的StringBuilders,我不完全理解,但这可能是因为一些内部优化它更快的原因。当我正在使用笔记本电脑时,我可能已经被电源管理状态更改所困扰,所以我使用'BuildCheat'方法重新运行代码(避免字符串构建器缓冲区容量加倍)并且时间非常接近String.Join(IEnumerable)(也在调试器之外运行)。

构建时间= 1264ms

JoinFormat = 1282ms

JoinConcat = 1108ms

BuildCheat = 1166ms

private static string BuildCheat<T>(
    IEnumerable<T> source,
    string delimiter,
    string prefix,
    string suffix)
{
    var builder = new StringBuilder(32);
    builder = builder.Append(prefix);

    using (var e = source.GetEnumerator())
    {
        if (e.MoveNext())
        {
            builder.Append(e.Current);
        }

        while (e.MoveNext())
        {
            builder.Append(delimiter);
            builder.Append(e.Current);
        }
    }

    builder.Append(suffix);
    return builder.ToString();
}

你的问题的最后一部分的答案就是你提到FastAllocateString的使用但正如你所看到的那样,在传递IEnumerable的重载方法中没有调用它,只有当它直接使用字符串时它才被调用,它绝对是在创建最终输出之前,它会遍历字符串数组以总结它们的长度。

public static unsafe string Join(string separator, string[] value, int startIndex, int count)
{
    if (value == null)
    {
        throw new ArgumentNullException("value");
    }
    if (startIndex < 0)
    {
        throw new ArgumentOutOfRangeException("startIndex", Environment.GetResourceString("ArgumentOutOfRange_StartIndex"));
    }
    if (count < 0)
    {
        throw new ArgumentOutOfRangeException("count", Environment.GetResourceString("ArgumentOutOfRange_NegativeCount"));
    }
    if (startIndex > (value.Length - count))
    {
        throw new ArgumentOutOfRangeException("startIndex", Environment.GetResourceString("ArgumentOutOfRange_IndexCountBuffer"));
    }
    if (separator == null)
    {
        separator = Empty;
    }
    if (count == 0)
    {
        return Empty;
    }
    int length = 0;
    int num2 = (startIndex + count) - 1;
    for (int i = startIndex; i <= num2; i++)
    {
        if (value[i] != null)
        {
            length += value[i].Length;
        }
    }
    length += (count - 1) * separator.Length;
    if ((length < 0) || ((length + 1) < 0))
    {
        throw new OutOfMemoryException();
    }
    if (length == 0)
    {
        return Empty;
    }
    string str = FastAllocateString(length);
    fixed (char* chRef = &str.m_firstChar)
    {
        UnSafeCharBuffer buffer = new UnSafeCharBuffer(chRef, length);
        buffer.AppendString(value[startIndex]);
        for (int j = startIndex + 1; j <= num2; j++)
        {
            buffer.AppendString(separator);
            buffer.AppendString(value[j]);
        }
    }
    return str;
}

出于兴趣,我改变了你的程序,不使用泛型,并使JoinFormat和JoinConcat接受一个简单的字符串数组(我不能轻易更改Build,因为它使用枚举器),所以String.Join使用上面的其他实现。结果令人印象深刻:

JoinFormat时间= 386ms

JoinConcat时间= 226ms

也许您可以找到一种能够充分利用通用输入的快速字符串数组的解决方案......

答案 1 :(得分:1)

为了提供一些额外的信息,我使用VS 2012运行了我的笔记本电脑(Core i7-2620M)上面的代码,并查看框架4.0和4.5之间是否有任何变化。第一次运行是针对.Net Framework 4.0编译的,然后是4.5。

Framework 4.0

构建时间= 1516毫秒

JoinFormat时间= 1407毫秒

JoinConcat时间= 1238ms

Framework 4.5

构建时间= 1421毫秒

JoinFormat时间= 1374毫秒

JoinConcat time = 1223ms

很高兴看到新框架看起来有点快,但很奇怪我无法通过JoinFormat的缓慢性能重现原始结果。您能否提供有关构建环境和硬件的详细信息?

答案 2 :(得分:-1)

尝试使用Build<T>方法中的StringBuilder.AppendFormat代替StringBuilder.Append

答案 3 :(得分:-2)

最简单的WorkAround(为字符串添加前缀和后缀):

string[] SelectedValues = { "a", "b", "c" };
string seperatedValues = string.Join("\n- ", SelectedValues);
seperatedValues = "- " + seperatedValues;

输出:
- a - b
- c

您可以使用字符串构建器