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 の組み合わせが最も遅いことです。この answerを検討した後、これはもう少し理にかなっています。 の出力はstring.Joinの外部によって処理されていますStringBuilderstring.Formatこのアプローチには固有の遅延があります。

string.Join熟考した後、どうすれば速くなるかがはっきりとわかりません。の使用について読んだことがありますが、のすべてのメンバーをFastAllocateString()呼び出さずにバッファを正確に事前に割り当てる方法がわかりません。Join-Concat の組み合わせが速いのはなぜですか?.ToString()sequence

それを理解したらunsafe string Join、余分なパラメーターとパラメーターを取り、「安全な」代替を実行する独自の関数prefixsuffix作成することは可能でしょうか。

私はいくつかの試みをしましたが、それらは機能しますが、高速ではありません。

4

4 に答える 4

4

元の質問に答えようとすると、答えは (驚くべき) リフレクター ツールにあると思います。IEnumerable であるオブジェクトのコレクションを使用しているため、String.Join メソッドで同じタイプのオーバーロードが呼び出されます。興味深いことに、この関数はコレクションを列挙し、すべての文字列の長さを事前に知る必要がないことを意味する文字列ビルダーを使用するため、ビルド関数と非常によく似ています。

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」メソッド (文字列ビルダーのバッファー容量が 2 倍になるのを回避する) を含めてコードを再実行しました。 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

おそらく、汎用入力を使用しながら、高速な文字列配列を最大限に活用するソリューションを見つけることができます...

于 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.AppendFormatを使用してみてくださいBuild<T>StringBuilder.Append

于 2012-11-16T15:34:24.793 に答える