Опубликован: 28.04.2009 | Доступ: свободный | Студентов: 1838 / 107 | Оценка: 4.36 / 4.40 | Длительность: 16:40:00
Специальности: Программист
Лекция 3:

Усложненные технологии визуализации

3.3.4. Замена циклов foreach на for

При визуализации диска проходы ( Passes ) эффекта перебираются посредством цикла foreach.

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

List<int> numbers = new List<int>(3);
...
int s = 0;
foreach (int v in numbers) s += v;

неявно заменяется компилятором следующим аналогом:

List<int> numbers = new List<int>(3);
...
int s = 0;
// Создается объект enumerator
IEnumerator enumerator = numbers.GetEnumerator(); 
// Перебирает элементы коллекции while
 (enumerator.MoveNext())
// Получает доступ к текущему элементу коллекции s
 += (int)enumerator.Current;

На первый взгляд, данный код является неэффективным. Но в действительности все не так уж и плохо. Во-первых, метод GetEnumerator возвращает структуру:

public class List<T> : IList<T>, 
ICollection<T>, IEnumerable<T>, IList,
ICollection,
IEnumerable
{
public Enumerator<T> 
GetEnumerator() {
return new Enumerator<T>((List<T>)
 this); }
...
public struct Enumerator : IEnumerator<T>, IDisposable,
 IEnumerator 
{
private List<T> list;
private int index;
private int version;
private T current;
internal Enumerator(List<T> list);
public void Dispose();
public bool MoveNext();
public T Current { get; }
object IEnumerator.Current { get; }
void IEnumerator.Reset(); }
}

Соответственно, вызов метода GetEnumerator не приводит к выделению памяти в управляемой куче и учащению вызовов сборщика мусора. Во-вторых, компилятор C# вызывает метод MoveNext и свойство Current структуры Enumerator напрямую без приведения к ссылке на интерфейс IEnumerator, так что боксирование ( boxing ) структуры Enumerator не происходит. И, наконец, компилятор может встроить ( inline ) код метода MoveNext и свойства Current непосредственно в код цикла, избежав накладных расходов на вызовы методов. Так что при использовании массивов или коллекций наподобие List разница в производительности циклов foreach и for будет исключающее мала, и в подавляющем большинстве случаев на нее можно не обращать внимания.

Однако при использовании других коллекций не все так просто. Например, у некоторых объектов метод GetEnumerator возвращает объект, размещаемых в управляемой куче. Подобный подход имеет два недостатка:

  1. Частый вызов цикла foreach для перебора элементов такой коллекции приведет к созданию множества объектов enumerator, и, соответственно, частому вызову сборщика мусора, что привет к падению производительности.
  2. Если метод MoveNext является виртуальным, то компилятор не сможет встроить его непосредственно в код цикла, что тоже негативно скажется на производительности.

Таким образом, в критичных к производительности приложениях при переборе элементов коллекций, возвращающих объект enumerator, имеет смысл избегать циклов foreach.

Но давайте вернемся к коллекции Passes класса Effect. Данная коллекция реализуется классом EffectPassCollection, объявленным следующим образом:

public sealed class EffectPassCollection :
 IEnumerable<EffectPass> 
{
// Список для хранения информации о проходах
 private List<EffectPass> pPass;
// Возвращает объект (полученный путем боксирования 
структуры), реализующий интерфейс 
// IEnumerator<EffectPass>
public IEnumerator<EffectPass> GetEnumerator()
{
return (IEnumerator<EffectPass>) this.pPass.GetEnumerator();
} 
}

Давайте внимательно рассмотрим этот код. Класс EffectPassCollection в действительности хранит информацию о проходах в коллекции List. Соответственно метод this.pPass. GetEnumerator возвращает ссылку на структуру Enumerator класса List, определение которой было приведено в начале раздела.

И все бы было просто замечательно, если бы не один маленький нюанс - структура IEnumerator приводится к интерфейсу IEnumerator<EffectPass>, что приводит к боксированию и выделению памяти в управляемой куче. Чтобы оценить влияние этой особенности на производительность приложения, мы встроим в обработчик события Paint код, перебирающий элементы коллекции 10.000.000 раз посредством циклов for и foreach с измерением времени их выполнения (листинг 3.22).

// Пример Examples\Ch03\Ex11 #define TEST
#if TEST
bool testing = true;
#endif
private void MainForm_Paint(object sender, PaintEventArgs e)
{
// Измерение выполняется только при условии определения 
идентификатора TEST
#if TEST
// Тест выполняется только один раз
if (testing)
{ 
// Число итераций
const int n = 10000000;
 int sum;
// Выводим количество сборок мусора, выполненных на момент 
начала эксперимента
 Trace.WriteLine("Gen 0 collection count: " + GC.CollectionCount(0));
Stopwatch timer = new Stopwatch(); 
// Запускаем таймер
timer.Start();
// Перебираем элементы коллекции passes и суммируем коды первой буквы
 sum = 0;
EffectPassCollection passes = effect.CurrentTechnique.Passes; 
// Выполняем n итераций
for (int i = 0; i < n; i++) 
// Перебираем элементы коллекции passes посредством 
for и суммируем коды первой буквы for
 (int j = 0; j < passes.Count; j++) sum += (int)passes[j].Name[0];
// Вычисляем суммарное время, затраченное на интеграции
double time1 = (double)timer.ElapsedTicks / (double)Stopwatch.Frequency;
// Отображаем отчет
Trace.WriteLine(" 	 for 	 ");
Trace.WriteLine("Sum : " + sum.ToString());
Trace.WriteLine("Gen 0 collection count: " + GC.CollectionCount(0));
Trace.WriteLine("Time1 : " + time1);
Trace.WriteLine("");
// Перезапускаем таймер
 timer.Reset(); timer.Start();
sum = 0; 
// Снова выполняем итерации, но уже посредством цикла foreach for
 (int i = 0; i < n; i++)
foreach (EffectPass pass in effect.CurrentTechnique.Passes) sum 
+= (int)pass.Name[0];
// Вычисляем суммарное время, потраченное на итерации
double time2 = (double)timer.ElapsedTicks / 
(double)Stopwatch.Frequency;
// Отображаем отчет
Trace.WriteLine(" 	 foreach 	 ");
Trace.WriteLine("Sum : " + sum.ToString());
Trace.WriteLine("Gen 0 collection count: " + GC.CollectionCount(0));
Trace.WriteLine("Time2 : " + time2); 
// Определяем разницу в 
производительности
Trace.WriteLine("Time2 / Time1 = " + time2 / time1);
Trace.WriteLine("");
testing = false; }
#endif 
}
Листинг 3.22.

Запустив модифицированное приложение на своем компьютере с процессором Intel Pentium-4 2.8C я получил следующие результаты:

Accuracy = 3,5730747380043E-10 sec
Gen 0 collection count: 3
	 for 	
Sum : 1120000000
Gen 0 collection count: 3
Time1 : 0,554915015846586
	 foreach 	
Sum : 1120000000
Gen 0 collection count: 915
Time2 : 1,84470303389776
Time2 / Time1 = 3,32429828211344

Как видно, в процессе работы цикла foreach сборщик мусора был вызван 915 раз13Результаты, полученные на других компьютерах, могут заметно отличаться. Так на моем втором домашнем компьютере с процессором Intel Core2 Due E6300 при выполнении циклов foreach сборщик мусора был вызван 229 раз. В целом же, частота вызовов сборщика мусора в первую очередь зависит от размера кэша второго уровня CPU. Например, размер кэша процессора Intel Core2 Due E6300 (2MB) в четыре раза больше, чем у Pentium-4 2.8C (512KB). Соответственно, сборщик мусора на Intel Core2 Due E6300 вызывается в 4 раза реже по сравнению с Pentium-4 2.8C (915/229 = 3.996)., а разница в производительности между циклами for и foreach достигла трехкратной величины. Таким образом, перебор элементов коллекции foreach в методе Paint, вызываемом около сотни раз в секунду, является не самой лучшей идеей. Особенно если учесть, что реальные приложения зачастую содержат десятки эффектов, а внезапная сборка мусора при визуализации кадра может привести заметному провалу производительности.

Примечание

На самом деле Microsoft здорово поработала над эффективностью сборщика мусора, и сейчас сборка мусора в поколении 0 обычно занимает порядка 1 мс. В частности, пренебрегая накладными затратами цикла foreach, можно прикинуть, что в нашем эксперименте каждая сборка мусора выполнялась в среднем не более чем за (1.84 - 0.55) / 915 = 0.0014 секунд. Тем не менее, рано или поздно частые сборки мусора могут спровоцировать сбор мусора в старших поколениях, который будет воспринят пользователем как внезапное "подтормаживание " приложения.

3.3.5. Устранение зависимости движений диска от производительности компьютера

Итак, после всех наших трудов движения диска стали по-настоящему плавными. Тем не менее, наше приложение все еще далеко от совершенства. Давайте попробуем представить, что произойдет, если приложение вдруг внезапно приостановит свою работу на несколько секунд, например, из-за повысившейся активности работы с файлом подкачки, плохо читаемого сектора на диске или какой-либо проблемы в драйвере устройства. После возобновления выполнения приложения оно экстраполирует прямолинейное движение диска на несколько секунд вперед и обнаружит, что он уже далеко вылетел за пределы ограничивающей стены. После чего шарик будет автоматически возращен обратно в один из углов прямоугольной границы. Разумеется, траектория движения диска при этом окажется нарушенной, ведь за это время диск должен был уже несколько раз отскочить от стены. Подобный эффект в меньшей степени проявляется и при незначительных провалах производительности, что в конечном счете, приводит несколько различному поведению приложения на разных компьютерах. Вообще данная особенность не является большим недостатком для нашего приложения, в конце концов, диск ведь движется по случайной траектории. Однако в реальных игровых приложениях такая реакция на внезапные провалы производительности неминуемо приведет к разнообразным "глюкам " игровой логики (проход сквозь препятствия и т.п.). Причем чем ниже производительность компьютера, тем вероятнее возникновение проблем.

В принципе для ликвидации данного недостатка можно было бы найти аналитическое выражение зависимости координат центра диска от времени. Однако такое решение будет пригодно лишь для самых простых случаев. Поэтому мы реализуем более универсальный подход: моделирование движений диска с некоторым фиксированным шагом, например, 0.005 секунды. Реализация данной технологии потребует лишь косметической правки обработчика события Idle (листинг 3.23).

// Дискретный шаг времени (в секундах), с которым выполняются расчеты
const float timeStep = 0.005f;
// Максимальный временной интервал между двумя
 вызовами обработчика события Idle (в секундах)
const float maxDelta = 5.0f;
void Application_Idle(object sender, EventArgs e)
{
...
// Определяем текущее время
double currentTime = (double)stopwatch.ElapsedTicks /
 (double)Stopwatch.Frequency;
// Если время между двумя вызовами обработчика Idle 
превышает maxDelta, корректируем lastTime 
// во избежание слишком длинной работы обработчика события 
Idle if (currentTime - lastTime > maxDelta) 
lastTime = currentTime - maxDelta;

// Моделируем движения шарика дискретным шагом времени timeStep 
while (lastTime + timeStep < currentTime) 
{
posX += speedX * timeStep; 
posY += speedY * timeStep;
lastTime += timeStep; }
Invalidate(); 
}
Листинг 3.23.

В самых запущенных случаях задержка между визуализацией кадров может достигать минуты и даже больше. Так как моделирование игровой логики интервала времни в несколько минут может занять достаточно существенно время, в приложении используется простой прием - игровая логика рассчитывается не более чем за 5 секунд игрового времени (константа maxDelta ). Для пользователя подобный трюк является незаметным, ведь он все равно не сможет спрогнозировать поведение приложения на значительный временной интервал.

Примечание

В интерактивных приложениях вроде автосимуляторов при возникновении длительной задержки между кадрами имеет смысл приостановить игровой процесс, ведь в течение задержки в несколько секунд автомобиль игрока наверняка слетит с трассы или столкнется с препятствием. Эффекта автоматической приостановки игрового процесса можно достичь путем присвоения константе maxDelta небольшого значения порядка 0.2 - 0.5 секунд.

3.4. Визуализация полупрозрачных примитивов

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

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

В общем случае итоговый цвет полупрозрачного объекта может быть определен по формуле:

C=c_s-\alpha+c_d-(1-\alpha)
( 3.1)

где

c - результирующий цвет

c_s - цвет полупрозрачного объекта

c_d - цвет фона позади объекта

\alpha - коэффициент поглощения, равный 0 для полностью прозрачных объектов, и 1 для непрозрачных.

Перейдем к моделированию эффекта полупрозрачности в XNA Framework. Как вы помните, в XNA Framework любой объект визуализируется с использованием простых примитивов, которые проходят через ряд стадий графического конвейера. В конечном счете, каждый примитив преобразуется в массив пикселей, которые заносятся в кадровый буфер. По сути, итоговый массив пикселей и есть сам объект, а кадровый буфер - его фон. Когда объект является непрозрачным, то пиксели просто копируются в кадровый буфер, затирая старые значения (собственно этим мы до сих пор и занимались). Если же объект является полупрозрачным, то разумно предположить, что приложение должно скомбинировать цвет пикселей объекта с пикселями кадрового буфера по формуле 3.1.

Андрей Леонов
Андрей Леонов

Reference = add reference, в висуал студия 2010 не могу найти в вкладке Solution Explorer, Microsoft.Xna.Framework. Его нету.