13

更新

按照奶酪先生的回答,似乎

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类的函数?


在提交我对这个问题的答案后,我开始分析哪些是表现最好的答案。

我在控制台类中编写了这段代码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 组合是最慢的。在考虑了这个答案之后,这更有意义了,输出string.Join正在由外部处理StringBuilderstring.Format这种方法存在固有的延迟。

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

一旦我理解了这一点,是否有可能编写我自己的unsafe string Join函数,它需要额外的prefixsuffix参数,并执行“安全”的替代方案。

我已经进行了几次尝试,虽然它们起作用了,但它们并没有更快。

4

4 回答 4

4

为了尝试回答您最初的问题,我认为答案在于(令人惊叹的)反射器工具。您正在使用 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

加入格式 = 1282 毫秒

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

也许您可以找到一种解决方案,既可以充分利用快速字符串数组,又可以使用通用输入……

于 2012-11-16T22:07:04.220 回答
1

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

框架 4.0

构建时间 = 1516ms

JoinFormat 时间 = 1407ms

JoinConcat 时间 = 1238ms

框架 4.5

构建时间 = 1421ms

JoinFormat 时间 = 1374ms

JoinConcat 时间 = 1223ms

很高兴看到新框架似乎更快了一些,但很好奇的是,由于 JoinFormat 的缓慢性能,我无法重现您的原始结果。您能否提供有关您的构建环境和硬件的详细信息?

于 2012-11-16T16:30:17.620 回答
-1

尝试在方法中使用StringBuilder.AppendFormatBuild<T>而不是StringBuilder.Append

于 2012-11-16T15:34:24.793 回答
-2

最简单的解决方法(为字符串添加前缀和后缀):

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

输出:
-a
-b
-c

您可以使用字符串生成器

于 2018-01-11T20:52:07.787 回答