Итераторы

Архитектура

Абстракция через итерацию — один из методов абстракции доступа к данным, получивший широкое распространение ввиду своей универсальности. Доступ к элементам коллекции осуществляется последовательно, через итератор.

Итерируемые коллекции в C# должны реализовывать интерфейс IEnumerable.

Оператор foreach

Для более удобной работы с итерируемыми коллекциями в языке предусмотрен оператор foreach.

IEnumerable<int> collection = new int[] { 2, 3, 4, 5, 6, 7, 8, 9 };

foreach (int i in collection)
  Console.WriteLine(i);	

Особенности использования с массивами

В случае итерации коллекций с произвольным доступом, например массивов, для доступа к элементам лучше воспользоваться оператором for. Для этого есть три веские причины:

Это следует учитывать при разработке систем, критичных к времени работы или на платформах, обладающих ограниченными вычислительными ресурсами и памятью, а так же высоконагруженных (облачных) приложений.

Расставим точки над ё

Посчитаем, сколько времени требуется для получения доступа к элементам через итератор и через индекс. Напишем простую программу, вызывающую определенное количество раз функцию, в которой элементы массива обходится тем или иным способом. Текст программы приведен в конце страницы.

Тест проведем в несколько этапов. На каждом этапе будем на порядок увеличивать количество элементов в массиве, и на порядок уменьшать количество циклов. Таким образом, можно посмотреть на корреляцию времени, в зависимости от размера коллекции и количества вызовов.

Тест времени доступа. Компьютер Intel Core2Duo 2.2.Ghz, 800 Mhz FSB
	
Элементов - 10, циклов 1000000
	______________________________________________
	Доступ итерированием.
	Тактов: 9305626
	Миллисекунд: 650
	
	Доступ по индексу.
	Тактов: 2938638
	Миллисекунд: 205
	
	
	Элементов - 100, циклов 100000
	______________________________________________
	Доступ итерированием.
	Тактов: 6130662
	Миллисекунд: 428
	
	Доступ по индексу.
	Тактов: 2450851
	Миллисекунд: 171
	
	
	Элементов - 1000, циклов 10000
	______________________________________________
	Доступ итерированием.
	Тактов: 5578007
	Миллисекунд: 389
	
	Доступ по индексу.
	Тактов: 2351414
	Миллисекунд: 164
	
	
	Элементов - 10000, циклов 1000
	______________________________________________
	Доступ итерированием.
	Тактов: 5555724
	Миллисекунд: 388
	
	Доступ по индексу.
	Тактов: 2352066
	Миллисекунд: 164
	
	
	Элементов - 100000, циклов 100
	______________________________________________
	Доступ итерированием.
	Тактов: 5597581
	Миллисекунд: 391
	
	Доступ по индексу.
	Тактов: 2361441
	Миллисекунд: 165
	
	
	Элементов - 1000000, циклов 10
	______________________________________________
	Доступ итерированием.
	Тактов: 5572387
	Миллисекунд: 390
	
	Доступ по индексу.
	Тактов: 2412830
	Миллисекунд: 170
	
	
	Элементов - 10000000, циклов 1
	______________________________________________
	Доступ итерированием.
	Тактов: 5570852
	Миллисекунд: 391
	
	Доступ по индексу.
	Тактов: 2438235
	Миллисекунд: 172

Как нетрудно заметить, разница есть. Причем на самом «рабочем» варианте, небольшой размер коллекции и большое количество циклов, разница в скорости работы составляет около трех раз. С уменьшением количества циклов разница во времени уменьшается и, начиная примерно с тысячи элементов, остаётся неизменной.

Почему с увеличением размера коллекции увеличивается время работы варианта с доступом по индексу? Наиболее вероятное объяснение — маленькая коллекция целиком ложится в кэш. Если бы объекты были не по четыре байта,  а размером в несколько килобайт, разница могла бы быть ощутимее. Но это вопрос отдельного исследования.

Текст программы

public class SpeedTest<T> where T:struct
	{
		private T [] _array;
		private IEnumerable<T> _collection;

		public SpeedTest(int power)
		{
			List<T> t = new List<T>(power);
			for (int i = 0; i < power; i++)
				t.Add(default(T));

			_array = t.ToArray();
			_collection = _array;
		}

		public int Power
		{
			get { return _array.Length; }
		}

		public void Enumerate()
		{
			T value = default(T);
			foreach (T i in _collection)
				value = i;
		}
		public void Index()
		{
			T value = default(T);
			for(int i = 0; i < _array.Length; i++)
				value = _array[i];
		}
	}

	class Program
	{
		public static void RunCycles(int count, string name, Action testFunction)
		{
			Console.WriteLine(name);
			System.Diagnostics.Stopwatch sw = new Stopwatch();

			sw.Start();
			for (int i = 0; i < count; i++)
				testFunction();

			Console.WriteLine(string.Format("Тактов: {0}", sw.ElapsedTicks));
			Console.WriteLine(string.Format("Миллисекунд: {0}", sw.ElapsedMilliseconds));
		}
		static void Main(string[] args)
		{
			for (int power = 1; power <= 7; power++)
			{
				int cp = (int)Math.Pow(10, power);
				int cycles = (int)Math.Pow(10, 7 - power);
				SpeedTest<int> t = new SpeedTest<int>(cp);
								
				Console.WriteLine(string.Format("Элементов - {0}, циклов {1}", t.Power, cycles));
				Console.WriteLine("______________________________________________");
				
				RunCycles(cycles, "Доступ итерированием.", t.Enumerate);
				Console.WriteLine();
				RunCycles(cycles, "Доступ по индексу.", t.Index);

				Console.WriteLine();
				Console.WriteLine();
				
			}
			Console.WriteLine("Нажмите любую клавишу для завершения работы.");
			Console.ReadKey();
		}
	}