34

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   
}

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

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

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

4

2 に答える 2

67

ほとんどの場合、答えは問題ではありません。 ループ内の項目の数 (「多数」の項目、たとえば数千の項目と見なされる場合でも) は、コードに影響を与えません。

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

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

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

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

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

MyObjectクラス:

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

List<T>任意の長さのMyClassインスタンスを作成する方法:

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 がデリゲートを使用し、影響を測定するために何かを呼び出す必要があるため必要):

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);
}

nullTextWriterに書き込む を作成するメソッド(基本的にはデータ シンク): Stream

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>またはインターフェイスに反対する場合としない場合があります)、そのwhile構造に巻き上げられます。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メソッド onのコードは次の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 と機能的に同等であることに注意してください。ただし、forループに巻き上げられるコードがデリゲートとして渡されるという 1 つの例外があります。これには、実行する必要があるコードに到達するために逆参照が必要です。デリゲートのパフォーマンスは .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

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

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

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

方法 1 で示したように、これはまさにコンパイラが行うことです (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

この特定のケースでは、リスト インデクサーが基になる配列に直接アクセスしてルックアップを実行するため、ある程度の速度が得られます (これは実装の詳細です。ところで、ツリー構造にすることはできません。バックアップし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

ただし、これ違いを生む可能性があるのは配列です。一度に複数の項目を処理するために、配列はコンパイラによって巻き戻されます。

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

既成のResharperforは、上記のステートメントをステートメントに変更するためのリファクタリングを含む提案を提供することに注意してforeachください。これが正しいと言っているわけではありませんが、基本はコードの技術的負債の量を減らすことです。


TL;DR

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

一般に、最もメンテナンスしやすい方法を選択する必要があります。その場合は、方法 1 ( foreach) が適しています。

于 2013-03-06T12:44:31.173 に答える