web-dev-qa-db-ja.com

C#で最も効率的なループは何ですか

C#のオブジェクトのアイテムを使用して同じ単純なループを実現する方法はいくつかあります。

このため、パフォーマンスや使いやすさなど、他のものを使用する理由があるのではないかと思うようになりました。それとも、個人的な好みだけです。

シンプルなオブジェクトを取ります

var myList = List<MyObject>; 

オブジェクトが満たされ、アイテムを反復処理するとします。

方法1。

foreach(var item in myList) 
{
   //Do stuff
}

方法2

myList.Foreach(ml => 
{
   //Do stuff
});

方法3

while (myList.MoveNext()) 
{
  //Do stuff
}

方法4

for (int i = 0; i < myList.Count; i++)
{
  //Do stuff   
}

私が思っていたのは、これらのそれぞれが同じものにコンパイルされているのですか?あるものを他のものよりも使用することのパフォーマンス上の明確な利点はありますか?

または、これはコーディング時の個人的な好みによるものですか?

見逃したことがありますか?

27
TheAlbear

ほとんどの場合の答えは、それは問題ではありません。ループ内の項目の数(「大きい」数のアイテム(数千単位など)はコードに影響を与えません。

もちろん、これをあなたの状況のボトルネックとして特定した場合は、必ず対処しますが、最初にボトルネックを特定する必要があります。

そうは言っても、それぞれのアプローチで考慮すべき点がいくつかありますが、ここではその概要を説明します。

最初にいくつかのことを定義しましょう:

  • すべてのテストは、32ビットプロセッサ上の.NET 4.0で実行されました。
  • _TimeSpan.TicksPerSecond_ 私のマシンで= 10,000,000
  • すべてのテストは、同じセッションではなく、個別のユニットテストセッションで実行されました(ガベージコレクションなどに干渉しないようにするため)

各テストに必要なヘルパーは次のとおりです。

MyObjectクラス:

_public class MyObject
{
    public int IntValue { get; set; }
    public double DoubleValue { get; set; }
}
_

任意の長さのMyClassインスタンスの _List<T>_ を作成するメソッド:

_public static List<MyObject> CreateList(int items)
{
    // Validate parmaeters.
    if (items < 0) 
        throw new ArgumentOutOfRangeException("items", items, 
            "The items parameter must be a non-negative value.");

    // Return the items in a list.
    return Enumerable.Range(0, items).
        Select(i => new MyObject { IntValue = i, DoubleValue = i }).
        ToList();
}
_

リスト内の各アイテムに対して実行するアクション(方法2はデリゲートを使用し、somethingを呼び出して影響を測定する必要があるため):

_public static void MyObjectAction(MyObject obj, TextWriter writer)
{
    // Validate parameters.
    Debug.Assert(obj != null);
    Debug.Assert(writer != null);

    // Write.
    writer.WriteLine("MyObject.IntValue: {0}, MyObject.DoubleValue: {1}", 
        obj.IntValue, obj.DoubleValue);
}
_

TextWriter を作成するメソッド nullStream (基本的にデータシンク)に書き込みます。

_public static TextWriter CreateNullTextWriter()
{
    // Create a stream writer off a null stream.
    return new StreamWriter(Stream.Null);
}
_

そして、アイテムの数を100万(1,000,000、これを強制するのに十分な高さである必要がありますが、これらはすべてほぼ同じパフォーマンスへの影響があります)に修正します。

_// The number of items to test.
public const int ItemsToTest = 1000000;
_

メソッドに入りましょう:

方法1:foreach

次のコード:

_foreach(var item in myList) 
{
   //Do stuff
}
_

以下にコンパイルします。

_using (var enumerable = myList.GetEnumerable())
while (enumerable.MoveNext())
{
    var item = enumerable.Current;

    // Do stuff.
}
_

そこにはかなりのことが起こっています。メソッドの呼び出しがあり(この場合、コンパイラはダックタイピングを尊重するため、_IEnumerator<T>_またはIEnumeratorインターフェースに反する場合としない場合があります)、_// Do stuff_が巻き上げられますその間構造。

パフォーマンスを測定するテストは次のとおりです。

_[TestMethod]
public void TestForEachKeyword()
{
    // Create the list.
    List<MyObject> list = CreateList(ItemsToTest);

    // Create the writer.
    using (TextWriter writer = CreateNullTextWriter())
    {
        // Create the stopwatch.
        Stopwatch s = Stopwatch.StartNew();

        // Cycle through the items.
        foreach (var item in list)
        {
            // Write the values.
            MyObjectAction(item, writer);
        }

        // Write out the number of ticks.
        Debug.WriteLine("Foreach loop ticks: {0}", s.ElapsedTicks);
    }
}
_

出力:

Foreachループティック:3210872841

方法2:_.ForEach_の_List<T>_メソッド

_.ForEach_の_List<T>_メソッドのコードは次のようになります。

_public void ForEach(Action<T> action)
{
    // Error handling omitted

    // Cycle through the items, perform action.
    for (int index = 0; index < Count; ++index)
    {
        // Perform action.
        action(this[index]);
    }
}
_

これは方法4と機能的に同等であることに注意してください。1つの例外を除き、forループに巻き上げられたコードはデリゲートとして渡されます。これには、実行する必要のあるコードを取得するための逆参照が必要です。デリゲートのパフォーマンスは.NET 3.0以降改善されましたが、そのオーバーヘッドはそこにあります

しかし、それはごくわずかです。パフォーマンスを測定するテスト:

_[TestMethod]
public void TestForEachMethod()
{
    // Create the list.
    List<MyObject> list = CreateList(ItemsToTest);

    // Create the writer.
    using (TextWriter writer = CreateNullTextWriter())
    {
        // Create the stopwatch.
        Stopwatch s = Stopwatch.StartNew();

        // Cycle through the items.
        list.ForEach(i => MyObjectAction(i, writer));

        // Write out the number of ticks.
        Debug.WriteLine("ForEach method ticks: {0}", s.ElapsedTicks);
    }
}
_

出力:

ForEachメソッドの目盛り:3135132204

foreachループを使用するよりも実際には〜7.5秒高速です。 _IEnumerable<T>_ を使用する代わりに直接配列アクセスを使用することを考えると、まったく驚くことではありません。

ただし、これは保存されるアイテムごとに0.0000075740637秒に変換されることに注意してください。それは、アイテムの小さなリストには価値がありませんnot

方法3:while (myList.MoveNext())

方法1に示すように、これはexactlyコンパイラーが行うことです(usingステートメントを追加します。これは良い習慣です)。ここでは、コンパイラーが生成するコードを自分で解くことによって何も得ていません。

キックについては、とにかくやってみましょう:

_[TestMethod]
public void TestEnumerator()
{
    // Create the list.
    List<MyObject> list = CreateList(ItemsToTest);

    // Create the writer.
    using (TextWriter writer = CreateNullTextWriter())
    // Get the enumerator.
    using (IEnumerator<MyObject> enumerator = list.GetEnumerator())
    {
        // Create the stopwatch.
        Stopwatch s = Stopwatch.StartNew();

        // Cycle through the items.
        while (enumerator.MoveNext())
        {
            // Write.
            MyObjectAction(enumerator.Current, writer);
        }

        // Write out the number of ticks.
        Debug.WriteLine("Enumerator loop ticks: {0}", s.ElapsedTicks);
    }
}
_

出力:

列挙子ループティック:3241289895

方法4:for

この特定のケースでは、リストインデクサーが基になる配列に直接アクセスしてルックアップを実行するため、ある程度の速度が得られます(実装の詳細、BTW、ツリー構造にできないことは言うまでもありません) _List<T>_のバックアップ)。

_[TestMethod]
public void TestListIndexer()
{
    // Create the list.
    List<MyObject> list = CreateList(ItemsToTest);

    // Create the writer.
    using (TextWriter writer = CreateNullTextWriter())
    {
        // Create the stopwatch.
        Stopwatch s = Stopwatch.StartNew();

        // Cycle by index.
        for (int i = 0; i < list.Count; ++i)
        {
            // Get the item.
            MyObject item = list[i];

            // Perform the action.
            MyObjectAction(item, writer);
        }

        // Write out the number of ticks.
        Debug.WriteLine("List indexer loop ticks: {0}", s.ElapsedTicks);
    }
}
_

出力:

リストインデクサーループティック:3039649305

ただし、このcanが違いを生じる場所は配列です。一度に複数のアイテムを処理するために、コンパイラは配列を巻き戻すことができます。

コンパイラは、10アイテムループで1つのアイテムを10回繰り返す代わりに、これを10アイテムループで2つのアイテムの5つの繰り返しに巻き戻すことができます。

しかし、私はこれが実際に起こっていることをここでは肯定的ではありません(ILとコンパイルされたILの出力を見なければなりません)。

テストは次のとおりです。

_[TestMethod]
public void TestArray()
{
    // Create the list.
    MyObject[] array = CreateList(ItemsToTest).ToArray();

    // Create the writer.
    using (TextWriter writer = CreateNullTextWriter())
    {
        // Create the stopwatch.
        Stopwatch s = Stopwatch.StartNew();

        // Cycle by index.
        for (int i = 0; i < array.Length; ++i)
        {
            // Get the item.
            MyObject item = array[i];

            // Perform the action.
            MyObjectAction(item, writer);
        }

        // Write out the number of ticks.
        Debug.WriteLine("Enumerator loop ticks: {0}", s.ElapsedTicks);
    }
}
_

出力:

配列ループティック:3102911316

すぐに使用できる Resharper は、上記のforステートメントをforeachステートメントに変更するリファクタリングを提案していることに注意してください。これが正しいと言うわけではありませんが、基本はコードの技術的負債の量を減らすことです。


TL; DR

実際のボトルネックがあることが状況でテストで示されていない限り、これらのパフォーマンスを気にする必要はありません(影響を与えるには大量のアイテムが必要です)。

一般的に、最も保守しやすいものを選ぶべきです。その場合、方法1(foreach)が道です。

54
casperOne

質問の最後の部分に関して、「私は何かを見逃しましたか?」はい、質問はかなり古いものですが、ここで言及しないのは怠慢だと思います。これらの4つの方法は比較的同じ時間で実行されますが、上記の方法ではすべての方法よりも高速に実行されます。実際、反復されるリストのサイズが大きくなると、かなり大きくなります。最後のメソッドとまったく同じ方法ですが、ループの条件チェックで.Countを取得する代わりに、ループを設定する前にこの値を変数に割り当て、代わりにそれを使用して、このようなものを残します

var countVar = list.Count;
for(int i = 0; i < countVar; i++)
{
 //loop logic
}

この方法で行うと、CountまたはLengthプロパティを解決するのではなく、各反復で変数値をルックアップするだけになり、効率が大幅に低下します。

2
nickw