web-dev-qa-db-ja.com

AddRangeがforeachループを使用するよりも高速なのはなぜですか?

var fillData = new List<int>();
for (var i = 0; i < 100000; i++)
{
     fillData.Add(i);
}

var stopwatch1 = new Stopwatch();
stopwatch1.Start();
var autoFill = new List<int>();
autoFill.AddRange(fillData);
stopwatch1.Stop();

var stopwatch2 = new Stopwatch();
stopwatch2.Start();
var manualFill = new List<int>();
foreach (var i in fillData)
{
    manualFill.Add(i);
}
stopwatch2.Stop();

4stopwach1およびstopwach2の結果を取得すると、stopwatch1の値は常にstopwatch2よりも低くなります。つまり、addrangeforeachよりも常に高速です。誰が理由を知っていますか?

56
HB MAAM

潜在的に、AddRangeは、渡された値がIListまたは_IList<T>_を実装する場所を確認できます。存在する場合、範囲内にある値の数、したがって割り当てる必要があるスペースの量を調べることができます... foreachループは数回再割り当てする必要がある場合があります。

さらに、割り当て後でも、_List<T>_は _IList<T>.CopyTo_ を使用して、基礎となる配列への一括コピーを実行できます(もちろん_IList<T>_を実装する範囲の場合)。

もう一度テストを試して、_List<T>_の代わりにfillDataEnumerable.Range(0, 100000)を使用すると、2つはほぼ同じ時間がかかると思います。

85
Jon Skeet

Addを使用している場合、デフォルトの開始サイズ10(IIRC)から内部配列のサイズを必要に応じて(2倍に)徐々に変更しています。使用する場合:

var manualFill = new List<int>(fillData.Count);

私はそれが根本的に変わることを期待しています(これ以上のサイズ変更/データコピーはありません)。

リフレクターから、AddRangeは倍増するのではなく、内部的にこれを行います。

ICollection<T> is2 = collection as ICollection<T>;
if (is2 != null)
{
    int count = is2.Count;
    if (count > 0)
    {
        this.EnsureCapacity(this._size + count);
        // ^^^ this the key bit, and prevents slow growth when possible ^^^
57
Marc Gravell

AddRangeは追加されたアイテムのサイズをチェックし、内部配列のサイズを一度だけ増加させるためです。

18

List AddRangeメソッドのリフレクターからの逆アセンブリには、次のコードがあります

ICollection<T> is2 = collection as ICollection<T>;
if (is2 != null)
{
    int count = is2.Count;
    if (count > 0)
    {
        this.EnsureCapacity(this._size + count);
        if (index < this._size)
        {
            Array.Copy(this._items, index, this._items, index + count, this._size - index);
        }
        if (this == is2)
        {
            Array.Copy(this._items, 0, this._items, index, index);
            Array.Copy(this._items, (int) (index + count), this._items, (int) (index * 2), (int) (this._size - index));
        }
        else
        {
            T[] array = new T[count];
            is2.CopyTo(array, 0);
            array.CopyTo(this._items, index);
        }
        this._size += count;
    }
}

ご覧のとおり、EnsureCapacity()呼び出しやArray.Copy()の使用などの最適化がいくつかあります。

6
Chamindu

AddRangeを使用する場合、コレクションは配列のサイズを一度増やしてから、値をそこにコピーできます。

foreachステートメントを使用して、コレクションはコレクションのサイズを複数回増やす必要があります。

Thrサイズを大きくすると、配列全体をコピーするのに時間がかかります。

5
juergen d

これは、ウェイターにビールを1杯持って行くように10回頼み、ビールを一度に10杯持ってくるように頼むようなものです。

あなたはより速いと思います:)

4
devdimi

これはメモリ割り当ての最適化の結果だと思います。 AddRangeの場合、メモリは1回のみ割り当てられ、各反復でforeachが再割り当てされます。

また、AddRangeの実装にいくつかの最適化があるかもしれません(たとえば、memcpy)

2
ili

これは、Foreachループがすべての値を追加して、ループが1つずつ取得しているためです。
AddRange()メソッドは、「チャンク」として取得するすべての値を収集し、指定された場所にそのチャンクを一度に追加します。

簡単に理解すると、市場から10個のアイテムのリストを持っているようなものです。これにより、すべてのアイテムを1つずつ、またはすべてを一度にすばやく取得できます。

1
jungNitesh

アイテムを手動で追加する前に、初期リストの容量を初期化してみてください。

var manualFill = new List<int>(fillData.Count); 
1
sll

AddRange拡張機能は各アイテムを反復処理しませんが、各アイテムを全体として適用します。 foreachと.AddRangeの両方に目的があります。 Addrangeはあなたの現在の状況でスピードのコンテストに勝ちます。

詳細はこちら:

Addrange Vs Foreach

0
Spencer