19

概要

大きなテキスト ファイルを処理しているときに、説明できない次の (予期しない) パフォーマンスの低下に遭遇しました。この質問に対する私の目的は次のとおりです。

  • 以下に説明する速度低下の原因を理解する
  • 大規模な非プリミティブ配列をすばやく初期化する方法を見つける

問題

  • 配列に非プリミティブ参照項目が含まれています
    • ではないint[]MyComplexType[]
    • MyComplexType構造体ではなくクラスです
    • MyComplexTypestringいくつかのプロパティが含まれています
  • 配列は事前に割り当てられています
  • 配列が大きい
  • 配列に代入せずにアイテムを作成して使用する場合、プログラムは高速です
  • アイテムを作成してから配列アイテムに割り当てると、プログラムの速度が大幅に低下します
    • 配列項目の代入は単純な参照代入であると予想していましたが、以下のテスト プログラムの結果に基づくと、そうではないようです。

テストプログラム

C#次のプログラムを検討してください。

namespace Test
{
    public static class Program
    {
        // Simple data structure
        private sealed class Item
        {
            public Item(int i)
            {
                this.Name = "Hello " + i;
                //this.Name = "Hello";
                //this.Name = null;
            }
            public readonly string Name;
        }

        // Test program
        public static void Main()
        {
            const int length = 1000000;
            var items = new Item[length];

            // Create one million items but don't assign to array
            var w = System.Diagnostics.Stopwatch.StartNew();
            for (var i = 0; i < length; i++)
            {
                var item = new Item(i);
                if (!string.IsNullOrEmpty(item.Name)) // reference the item and its Name property
                {
                    items[i] = null; // do not remember the item
                }
            }
            System.Console.Error.WriteLine("Without assignment: " + w.Elapsed);

            // Create one million items and assign to array
            w.Restart();
            for (var i = 0; i < length; i++)
            {
                var item = new Item(i);
                if (!string.IsNullOrEmpty(item.Name)) // reference the item and its Name property
                {
                    items[i] = item; // remember the item
                }
            }
            System.Console.Error.WriteLine("   With assignment: " + w.Elapsed);
        }
    }
}

2 つのほぼ同一のループが含まれています。ループごとに、Itemクラスのインスタンスが 100 万個作成されます。最初のループは、作成された項目を使用してから、参照を破棄します (items配列に保持しません)。2 番目のループは、作成された項目を使用し、参照をitems配列に格納します。配列アイテムの割り当ては、ループ間の唯一の違いです。

私の結果

  • Releaseマシンでビルド (最適化をオン)を実行すると、次の結果が得られます。

    Without assignment: 00:00:00.2193348
       With assignment: 00:00:00.8819170
    

    配列代入のあるループは、代入なしのループよりも大幅に遅くなります (~4 倍遅くなります)。

  • プロパティItemに定数文字列を割り当てるようにコンストラクターを変更すると、次のようになります。Name

    public Item(int i)
    {
        //this.Name = "Hello " + i;
        this.Name = "Hello";
        //this.Name = null;
    }
    

    次の結果が得られます。

    Without assignment: 00:00:00.0228067
       With assignment: 00:00:00.0718317
    

    割り当てのあるループは、割り当てのないループよりも 3 倍遅くなります

  • null最後に、Nameプロパティに割り当てると:

    public Item(int i)
    {
        //this.Name = "Hello " + i;
        //this.Name = "Hello";
        this.Name = null;
    }
    

    次の結果が得られます。

    Without assignment: 00:00:00.0146696
       With assignment: 00:00:00.0105369
    

    文字列が割り当てられなくなると、割り当てのないバージョンは最終的にわずかに遅くなります (これらのインスタンスはすべてガベージ コレクションのために解放されるためだと思います)。

質問

  • 配列項目の割り当てがテスト プログラムを非常に遅くするのはなぜですか?

  • 割り当てを高速化する属性/言語構造/などはありますか?

PS: dotTrace を使用してスローダウンを調査しようとしましたが、決定的ではありませんでした。私が目にしたことの 1 つは、代入を使用しないループよりも、代入を使用するループの方が、文字列のコピーとガベージ コレクションのオーバーヘッドがはるかに多いことでした (私はその逆を予想していましたが)。

4

6 に答える 6

2

私の推測では、コンパイラは非常にスマートであり、Item を割り当てていない場合は Item で重要なことを何もする必要がないことがわかります。可能であるため、最初のループで Item オブジェクトのメモリを再利用するだけです。2 番目のループでは、ヒープのビットを割り当てる必要があります。それらはすべて独立しており、後で参照されるためです。

この種のことは、ガベージ コレクションに関連して見たことと一致すると思います。最初のループで 1 つのアイテムが作成されるのに対し、多数のアイテムが作成されます。

簡単なメモ - 最初のループは、最適化としてオブジェクト プーリングを使用している可能性があります。 この記事は洞察を提供するかもしれません。Reed がすぐに指摘するように、この記事ではアプリの最適化について述べていますが、アロケータ自体にも同様のことを行う多くの最適化があると思います。

于 2013-11-05T20:18:07.063 に答える
1

これが(実際には)配列の割り当てと関係があるとは思いません。後で参照する場合に備えて、アイテムとそれに含まれるオブジェクトを保持する必要がある時間の長さと関係があります。これは、ヒープ割り当てとガベージ コレクションの生成に関係しています。

最初に割り当てられたとき、そのitem文字列は「ジェネレーション 0」になります。これはしばしばガベージコレクションされ、非常にホットで、キャッシュされている可能性さえあるメモリです。ループの次の数回の繰り返しで、「ジェネレーション 0」全体が GC され、メモリが newitemsとその文字列に再利用される可能性が非常に高くなります。割り当てを配列に追加すると、オブジェクトへの参照がまだ存在するため、オブジェクトをガベージ コレクションできません。これにより、メモリ消費量が増加します。

コードの実行中にメモリが増加することがわかると思います。問題は、常に「新しい」メモリを使用する必要があり、ハードウェア メモリ キャッシュの恩恵を受けられないため、ヒープ内のメモリ割り当てとキャッシュ ミスが組み合わされていることだと思います。

于 2013-11-05T20:31:46.340 に答える
0

私の意見では、あなたは分岐予測の犠牲者です。あなたがしていることを詳細に見てみましょう:

「割り当てなし」の場合は、配列itemsのすべての要素にnullを割り当てるだけです。そうすることで、プロセッサーは for ループを何度か繰り返した後、配列項目に同じ値 ( nullも含む) を割り当てていることを学習します。したがって、ifステートメントはもう必要ありません。プログラムはより高速に実行されます。

「割り当てあり」の場合、プロセッサは新しく生成されたアイテムの進行を認識しません。ifステートメントforループの各反復で呼び出されます。これにより、プログラムの実行が遅くなります...

この動作は、分岐予測ユニットと呼ばれるプロセッサ ハードウェアの一部に依存しています (チップのトランジスタのかなりの部分を消費します...)。同様のトピックがここでよく説明されています。

于 2013-11-05T21:50:14.720 に答える