List<int> list = ...
for(int i = 0; i < list.Count; ++i)
{
...
}
Значит, компилятор знает список .Count не нужно вызывать каждую итерацию?
List<int> list = ...
for(int i = 0; i < list.Count; ++i)
{
...
}
Значит, компилятор знает список .Count не нужно вызывать каждую итерацию?
Вы уверены в этом?
List<int> list = new List<int> { 0 };
for (int i = 0; i < list.Count; ++i)
{
if (i < 100)
{
list.Add(i + 1);
}
}
Если компилятор кэшировал свойство Count
выше, содержимое list
будет равно 0 и 1. Если это не так, содержимое будет целым числом от 0 до 100.
Теперь это может показаться вам надуманным примером; но как насчет этого?
List<int> list = new List<int>();
int i = 0;
while (list.Count <= 100)
{
list.Add(i++);
}
Может показаться, что эти два фрагмента кода совершенно разные, но это происходит только из-за того, что мы склонны думать о циклах for
против циклов while
. В любом случае значение переменной проверяется на каждой итерации. И в любом случае это значение очень хорошо может измениться.
Как правило, небезопасно предположить, что компилятор оптимизирует что-то, когда поведение между "оптимизированными" и "неоптимизированными" версиями одного и того же кода на самом деле отличается.
Компилятор С# не выполняет никаких таких оптимизаций. Однако JIT-компилятор оптимизирует это для массивов, я считаю (которые не изменяются по размеру), но не для списков.
Свойство count списка может меняться внутри структуры цикла, поэтому это будет некорректная оптимизация.
Стоит отметить, как никто другой не упомянул об этом, что нет никакого знания смотреть на такой цикл, что на самом деле делает свойство "Count", или на какие побочные эффекты он может иметь.
Рассмотрим следующие случаи:
Сторонняя реализация свойства под названием "Count" может выполнять любой код, который он хотел. например верните случайное число для всех, кого мы знаем. Со списком мы можем быть немного более уверенными в том, как он будет работать, но как JIT рассказать об этих реализациях отдельно?
Любой вызов метода в цикле может потенциально изменить возвращаемое значение Count (а не только прямое "Добавить" непосредственно в коллекции, но метод пользователя, который вызывается в цикле, также может участвовать в коллекции)
Любой другой поток, который будет выполняться одновременно, также может изменить значение Count.
JIT просто не может "знать", что Count является постоянным.
Однако компилятор JIT может сделать код более эффективным с помощью вложения реализации свойства Count (пока это тривиальная реализация). В вашем примере это может быть сведено до простой проверки значения переменной, избегая накладных расходов на вызов функции на каждой итерации и тем самым делая конечный код приятным и быстрым. (Примечание: я не знаю, сделает ли JIT это, просто чтобы это могло быть. Мне все равно - см. Последнее предложение моего ответа, чтобы выяснить, почему)
Но даже с инкрустацией значение все равно может быть изменено между итерациями цикла, поэтому для каждого сравнения все равно нужно будет считывать из ОЗУ. Если вы скопировали Count в локальную переменную, и JIT мог бы определить, просмотрев код в цикле, что локальная переменная останется постоянной для времени цикла, тогда она сможет продолжить ее оптимизацию (например, удерживая константу значение в регистре, а не чтение его из ОЗУ на каждой итерации). Итак, если вы (как программист) знаете, что Count будет постоянным для жизни цикла, вы можете помочь JIT, используя кеширование Count в локальной переменной. Это дает JIT лучший шанс оптимизировать цикл. (Но нет никаких гарантий того, что JIT действительно применит эту оптимизацию, поэтому для выполнения "вручную" оптимизации времени может не иметь никакого значения, так что вы рискуете, что что-то пойдет не так, если ваше предположение (значение Count постоянное) неверно Или ваш код может сломаться, если другой программист отредактирует содержимое цикла, чтобы граф больше не был постоянным, и он не обнаруживает вашу умность)
Итак, мораль этой истории: JIT может сделать довольно хороший удар по оптимизации этого случая путем вложения. Даже если он не делает этого сейчас, он может сделать это со следующей версией С#. Вы не можете получить какое-либо преимущество, вручную "выбирая" код, и вы рискуете изменить его поведение и тем самым нарушить его или, по крайней мере, сделать дальнейшее обслуживание вашего кода более рискованным или, возможно, проиграть будущие улучшения JIT. Таким образом, лучший подход заключается в том, чтобы просто написать его так, как вы, и оптимизировать его, когда ваш профилировщик говорит вам, что цикл является узким местом вашей производительности.
Следовательно, имхо интересно рассмотреть/понять такие случаи, но в конечном итоге вам не нужно знать. Немного знаний может быть опасной. Просто дайте JIT сделать свое дело, а затем профилируйте результат, чтобы увидеть, нужно ли ему улучшать.
Если вы посмотрите на IL, сгенерированный для примера Dan Tao, вы увидите такую строку при условии цикла:
callvirt instance int32 [mscorlib]System.Collections.Generic.List`1<int32>::get_Count()
Это неоспоримое доказательство того, что граф (т.е. get_Count()) вызывается для каждой итерации цикла.
Для всех других комментаторов, которые говорят, что свойство "Count" может измениться в теле цикла: оптимизация JIT позволяет вам использовать фактический код, который работает, а не худший случай того, что может произойти. В общем, граф мог измениться. Но это не во всем коде.
Итак, в примере с плакатом (который, возможно, не имеет изменения Count), неразумно ли JIT обнаруживать, что код в цикле не изменяет ни одну внутреннюю переменную List, чтобы удерживать ее длину? Если он обнаруживает, что list.Count
является постоянным, не приведет ли он к выходу этого переменного доступа из тела цикла?
Я не знаю, делает ли JIT это или нет. Но я не так быстро убираю эту проблему, как тривиально "никогда".
Нет, это не так. Потому что условие вычисляется на каждом шаге. Это может быть более сложным, чем просто сравнение с count, и любое логическое выражение разрешено:
for(int i = 0; new Random().NextDouble() < .5d; i++)
Console.WriteLine(i);
http://msdn.microsoft.com/en-us/library/aa664753(VS.71).aspx
Это зависит от конкретной реализации Count; Я никогда не замечал никаких проблем с производительностью с использованием свойства Count в списке, поэтому я предполагаю, что это нормально.
В этом случае вы можете сэкономить себе написание с помощью foreach.
List<int> list = new List<int>(){0};
foreach (int item in list)
{
// ...
}