13

提示された質問を調査していました。文字列を入力として受け取り、文字間にスペースを含む文字列を返す関数を作成する方法。この関数は、1 秒間に何千回も呼び出されたときにパフォーマンスを最適化するように作成されます。

  1. .net には という関数がString.Joinあり、元の文字列と共にスペース文字を区切り文字として渡すことができることを知っています。

  2. を使用しないString.Join限り、StringBuilderクラスを使用して各文字の後にスペースを追加できます。

  3. このタスクを達成する別の方法は、2*n-1 文字で文字配列を宣言することです (スペースには n-1 文字を追加する必要があります)。文字配列は、ループで埋めてから String に渡すことができますconstructor

私は、これらの各アルゴリズムをパラメーターを使用してそれぞれ 100 万回"Hello, World"実行し、実行にかかる時間を測定する .net コードを作成しました。方法 (3) は、(1) または (2) よりもはるかに高速です。

(3) は、ガベージ コレクションの対象となる追加の文字列参照を作成しないため、非常に高速であることはわかっていますが、 のような組み込みの .net 関数を使用すると、String.Joinパフォーマンスが向上するはずです。String.Join手動で作業を行うよりも、使用が非常に遅いのはなぜですか?

public static class TestClass
{
    // 491 milliseconds for 1 million iterations
    public static string Space1(string s) 
    {            
        return string.Join(" ", s.AsEnumerable());
    }

    //190 milliseconds for 1 million iterations
    public static string Space2(string s) 
    {
        if (s.Length < 2)
            return s;
        StringBuilder sb = new StringBuilder();
        sb.Append(s[0]);
        for (int i = 1; i < s.Length; i++)
        {
            sb.Append(' ');
            sb.Append(s[i]);
        }            
        return sb.ToString();
    }

    // 50 milliseconds for 1 million iterations
    public static string Space3(string s) 
    {
        if (s.Length < 2)
            return s;
        char[] array = new char[s.Length * 2 - 1];
        array[0] = s[0];
        for (int i = 1; i < s.Length; i++)
        {
            array[2*i-1] = ' ';
            array[2*i] = s[i];
        }
        return new string(array);
    }

更新:プロジェクトを「リリース」モードに変更し、それに応じて質問の経過時間を更新しました。

4

6 に答える 6

7

String.Join を使用すると、手動で作業を行うよりもはるかに遅いのはなぜですか?

この場合String.Join遅い理由は、あなたの正確な性質の事前知識を持つアルゴリズムを書くことができるからです.IEnumerable<T>

String.Join<T>(string, IEnumerable<T>)一方、(使用しているオーバーロード)は、任意の列挙可能な型で動作することを目的としています。つまり、適切なサイズに事前に割り当てることはできません。この場合、純粋なパフォーマンスと速度のために柔軟性をトレードしています。

フレームワーク メソッドの多くは、条件をチェックすることで処理速度を上げることができる特定のケースを処理しますが、これは通常、その「特別なケース」が一般的になる場合にのみ行われます。

この場合、手書きのルーチンの方が高速なエッジ ケースを効果的に作成していますが、これは .NET の一般的なユース ケースではありませんString.Join。この場合、必要なものが正確に事前にわかっているため、正確なサイズの配列を事前に割り当て、結果を手動で構築することで、柔軟な設計に必要なすべてのオーバーヘッドを回避できます。

一般に、特定の入力データに対していくつかのフレームワーク ルーチンを実行するメソッドを記述できることがよくあります。フレームワーク ルーチンは任意のデータセットで動作する必要があるため、これは一般的です。つまり、特定の入力シナリオに合わせて最適化することはできません。

于 2012-06-14T17:30:25.237 に答える
4

あなたのString.Join例はで動作しますIEnumerable<char>IEnumerable<T>withの列挙はforeach、ループの実行よりも遅いことがよくありforます(Dave Blackがコメントで指摘したように、コレクションの種類やその他の状況によって異なります)。Joinを使用する場合でも、追加する項目の数が事前にわからないためStringBuilder、の内部バッファをStringBuilder数倍に増やす必要があります。

于 2012-04-05T17:08:44.577 に答える
3

リリース ビルド (デフォルトで最適化がチェックされている必要があります) を使用していないか、Visual Studio を介してデバッグしているため、JITer は多くの最適化を行うことができません。このため、各操作に実際にかかる時間を正確に把握できていません。最適化を追加すると、何が起こっているのかを実際に把握できます。

Visual Studio でデバッグしないことも重要です。bin/release フォルダーに移動し、Visual Studio の外部で実行可能ファイルを完全にダブルクリックします。

于 2012-04-05T17:07:35.743 に答える
2

最初のメソッドではString.Join、Enumerable で動作するオーバーロードを使用しています。これには、メソッドが列挙子を使用して文字列の文字をウォークする必要があります。内部的にはStringBuilder、正確な文字数が不明であるため、これは a を使用します。

String.Join代わりに文字列 (または文字列配列) を取るオーバーロードの使用を検討しましたか? その実装により、固定長のバッファーを (3 番目の方法と同様に) 使用できるようになり、速度のために内部の安全でない文字列操作も使用できます。コールは次のように変わります - String.Join(" ", s); 実際に測定するためのレッグワークを行わなくても、これは 3 番目のアプローチと同等かそれよりも速いと予想されます。

于 2012-04-05T17:28:16.093 に答える
1

出来の悪さは からではなくString.Join、各キャラクターの扱い方から来ています。この場合、文字を個別に処理する必要があるため、最初のメソッドはより多くの中間文字列を作成し、2 番目のメソッドは.Append文字ごとに 2 つのメソッド呼び出しに悩まされます。3 番目の方法は、多くの中間文字列やメソッド呼び出しを必要としないため、3 番目の方法が最も高速です。

于 2012-04-05T17:12:34.283 に答える
0

IEnumerabletoを渡したときString.Join、どのくらいのメモリを割り当てる必要があるのか​​ わかりません。メモリのチャンクを割り当て、不十分な場合はサイズを変更し、すべての文字列を収容するのに十分なメモリが得られるまでプロセスを繰り返します。

事前に割り当てられたメモリの量がわかっているため、配列バージョンの方が高速です。

また、最初のバージョンを実行している場合、GC が発生している可能性があることに注意してください。

于 2012-04-05T17:14:24.257 に答える