Подтвердить что ты не робот

Каков наиболее эффективный цикл в С#

Существует несколько разных способов выполнения одного и того же простого цикла, хотя элементы объекта в С#.

Это заставило меня задуматься о том, есть ли какая-либо причина, будь то производительность или простота использования, как использовать друг друга. Или это зависит от личных предпочтений.

Возьмите простой объект

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   
}

Что мне было интересно, так это каждый из них скомпилирован до одного и того же? есть ли явное преимущество в производительности для использования одного над другими?

или это просто до личного предпочтения при кодировании?

Я что-то пропустил?

4b9b3361

Ответ 1

Ответ в большинстве случаев не имеет значения. Количество элементов в цикле (даже то, что можно считать "большим" количеством элементов, скажем, в тысячах), является не будет влиять на код.

Конечно, если вы идентифицируете это как узкое место в вашей ситуации, непременно обратитесь к нему, но сначала вам нужно определить узкое место.

Тем не менее, есть много вещей, которые следует учитывать при каждом подходе, который я опишу здесь.

Сначала определите несколько вещей:

  • Все тесты выполнялись на .NET 4.0 на 32-разрядном процессоре.
  • TimeSpan.TicksPerSecond на моей машине = 10 000 000
  • Все тесты проводились в отдельных сеансах unit test, а не в одном (чтобы не мешать сборке мусора и т.д.).

Вот некоторые помощники, необходимые для каждого теста:

Класс 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);
}

Способ создания TextWriter, который записывается в null Stream (в основном, приемник данных):

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

И пусть исправить количество предметов в миллион (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, за одним исключением, код, который был вставлен в цикл 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

Это на самом деле ~ 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

В этом конкретном случае вы получите некоторую скорость, поскольку индексный указатель переходит непосредственно к базовому массиву для выполнения поиска (что деталь реализации, 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

Однако место, где это может иметь значение, это массивы. Компилятор может разматывать массивы для обработки нескольких элементов за раз.

Вместо того, чтобы делать десять итераций одного элемента в цикле из десяти элементов, компилятор может развернуть это на пять итераций двух элементов в десяти циклах элементов.

Однако, я не уверен, что это происходит на самом деле (я должен посмотреть на 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) - это путь.

Ответ 2

Относительно последнего бита вопроса: "Я что-то пропустил?" да, и я чувствую, что было бы упущением не упомянуть здесь, хотя вопрос довольно старый. Хотя эти четыре способа выполнения будут выполняться в относительно одинаковое время, их способ, не показанный выше, работает быстрее, чем все из них, что на самом деле весьма значительно, поскольку размер списка, который повторяется, увеличивается. Это будет точно так же, как и в последнем методе, но вместо получения .Count в проверке условий цикла вы присваиваете это значение переменной перед настройкой цикла и используете его вместо этого, оставляя вас с чем-то вроде этого

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

делая это таким образом, вы только просматриваете значение переменной на каждой итерации, а не разрешаете свойства Count или Length, что значительно менее эффективно.