Ответ 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
) - это путь.