Подтвердить что ты не робот

Самый быстрый способ соединения строк с префиксом, суффиксом и разделителем

UPDATE

Следуя Mr Cheese 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?


После отправки моего ответа на этот вопрос я был вовлечен в какой-то анализ, который будет лучшим ответом.

Я написал этот код в консольном классе 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();
    }
}

запуск кода в конфигурации выпуска, построенный с оптимизацией, из командной строки я получаю такой вывод.

...

Время сборки = 1555 мс

Время JoinFormat = 1715 мс

Время присоединения = 1452мс

Единственный сюрприз здесь (для меня) заключается в том, что комбинация Join-Format является самой медленной. Рассмотрев этот ответ, это имеет немного больший смысл, вывод string.Join обрабатывается внешним StringBuilder в string.Format, существует задержка с этим подходом.

После размышлений я не понимаю, как string.Join может быть быстрее. Я читал о его использовании FastAllocateString(), но я не понимаю, как буфер можно точно предварительно выделить без вызова .ToString() для каждого члена sequence. Почему комбинация Join-Concat работает быстрее?

Как только я это понимаю, можно ли написать собственную функцию unsafe string Join, которая принимает дополнительные параметры prefix и suffix, а out выполняет "безопасные" альтернативы.

У меня было несколько попыток, и пока они работают, они не быстрее.

4b9b3361

Ответ 1

Чтобы ответить на ваш первоначальный вопрос, я думаю, что ответ лежит в (удивительном) инструменте 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) (также выходил за пределы отладчика).

Время сборки = 1264 мс

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;
}

Просто из интереса я изменил вашу программу, чтобы не использовать generics, и сделал JoinFormat и JoinConcat принимающим простой массив строк (я не мог легко изменить Build, так как он использует перечислитель), поэтому String.Join использует другую реализацию выше, Результаты довольно впечатляющие:

JoinFormat time = 386ms

Время JoinConcat = 226ms

Возможно, вы можете найти решение, которое сделает лучшее из быстрых массивов строк, в то же время используя общие входы...

Ответ 2

Чтобы предоставить дополнительную информацию, я выполнил код выше на моем ноутбуке (Core i7-2620M), используя VS 2012, а также посмотреть, изменилось ли что-то между фреймворками 4.0 и 4.5. Первый запуск скомпилирован в .NET Framework 4.0 и затем 4.5.

Framework 4.0

Время сборки = 1516 мс

Время JoinFormat = 1407 мс

Время присоединения = 1238 мс

Framework 4.5

Время сборки = 1421 мс

Время Объединения = 1374 мс

Время присоединения = 1223 мс

Хорошо видеть, что новая структура кажется немного быстрее, но мне любопытно, что я не могу воспроизвести ваши исходные результаты с медленной производительностью JoinFormat. Можете ли вы предоставить подробную информацию о вашей среде сборки и оборудовании?

Ответ 3

Попробуйте использовать StringBuilder.AppendFormat в Build<T> вместо StringBuilder.Append

Ответ 4

Самый простой WorkAround (добавить префикс и суффикс к строке):

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

Вывод:
- a - b
- c

Вы можете использовать построитель строк