Загрузка...
 
Печать
ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Знакомство с XNA Framework и XNA Game Studio  »  XNA - Способы оптимизации

XNA Game Studio: Способы оптимизации




Здесь мы рассмотрим различные способы оптимизации, которые можно применить в случаях, когда определённый кусок исходного кода выполняется не так быстро, как хотелось бы.1 Все приведённые здесь способы относятся к т.н. "микрооптимизациям" (micro-optimization) и должны применяться только после предварительных замеров производительности, с целью убедиться, что данная оптимизация здесь реально необходима. Впадать в крайность, пытаясь выиграть каждый такт процессора в коде, где это реально не нужно - пустая трата времени и часто приводит к снижению читаемости исходного кода. Как, впрочем, и появлению новых багов.
В то же время выполнение микрооптимизаций очень важно, хоть и делать это надо лишь на последнем этапе написания кода. Одно из самых подходящих мест для них - вложенные циклы (nested loops). К примеру, в шаблонном приложении XNA методы Update и Draw размещены внутри главного цикла приложения и также имеют свои собственные вложенные циклы, например обновляющие логику искусственного интеллекта (AI logic), проверяющие физику и т.д.. Даже внутри этих компонентных проверок часто есть вложенные циклы. Именно в этих участках кода мы и будем производить микрооптимизации. Но сперва займёмся замерами.

Создание фреймворка для микрозамеров (Micro-Benchmark Framework)

Когда мы пытаемся сделать быстрее определённый кусок кода, то обычно причиной этого служит заметно больший объём времени, который он затрачивает на своё выполнение, по сравнению с другими элементами. Для этого необходимо сравнить различные реализации выполнения одной и той же задачи, выполняемой в проверяемом участке кода. Здесь нам поможет микрозамерное тестирование (micro-benchmarking testing).
Микрозамерное тестирование позволяет нам ближе взглянуть на то, как быстро выполняются маленькие порции кода. Важно при этом помнить несколько основных моментов:
  • Мы не можем со 100% точностью определить наилучший подход (practice) для выполнения микрозамерного тестирования, т.к. в каждом случае он будет разный.
  • Проверяемый кусок кода может выполняться быстрее в микрозамерном тесте и в то же время жрать память похлеще серверной СУБД, что, в свою очередь, приведёт к более частым вызовам сборщика мусора.
Суть в том, что микрозамерное тестирование само по себе очень эффективно и мы должны выполнять его (вот почему мы создадим для него целый фреймворк), мы должны осторожно относиться к любым предположениям (assumptions), сделанным на основе результатов проведённых измерений.
XNA Game Studio помимо игровых проектов также позволяет создавать проекты библиотек (library projects). После компилирования библиотечные проекты могут использоваться в других приложениях: игре, форме Windows (Windows Form) или даже в консольном приложении.
Несмотря на то, что наша цель - создание приложения фреймворка, сейчас мы создадим обычный проект игровой библиотеки Windows (Windows Game Library project). Назовём его XNAPerfomanceChecker. Главный класс назовём CheckPerfomance. Мы будем использовать данную библиотеку через консольное приложение.

Создаём библиотеку

  • Стартуй MSVC#2008, если не сделал этого ранее. Создай новый проект.
Где взять и как установить Microsoft Visual C# 2008 читай здесь: здесь(external link).
Как её подружить с XNA Framework читай здесь(external link).
  • В New Project в разделе Project types укажи XNA Game Studio 3.1 или самую последнюю версию. В правой части, в разделе Templates выбери Windows Game Library 3.1. В поле Name укажи имя проекта XNAPerfomanceChecker и нажми OK.

Спустя несколько секунд MSVC#2008 сгенерит шаблон Решения игровой библиотеки Windows и в правой части, в Solution Explorer, появится её древовидная структура. В левой части открыт основной файл исходного кода Class1.cs .
  • Переименуй файл исходного кода Class1.cs в CheckPerformance.cs . Для этого в правой части, в Solution Explorer, щёлкни правой кнопкой мыши по файлу Class1.cs и во всплывающем меню выбери Rename. Введи имя CheckPerformance.cs и нажми Enter на клавиатуре.

Появится сообщение, предлагающее также изменить имя главного класса в исходном коде на новое.
  • Жмём 'Да'.

Теперь в исходном коде главный класс библиотеки называется CheckPerformance.

  • Удали весь исходный код в CheckPerformance.cs и замени его на следующий:
CheckPerformance.cs
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace XNAPerformanceChecker
{
    public class CheckPerformance
    {
        private Vector3 cameraReference = new Vector3(0, 0, -1.0f);
        private Vector3 cameraPosition = new Vector3(0, 0, 3.0f);
        private Vector3 cameraTarget = Vector3.Zero;
        private Vector3 vectorUp = Vector3.Up;
        private Matrix projection;
        private Matrix view;
        private float cameraYaw = 0.0f;

        public CheckPerformance() { }

        public void TransformVectorByValue()
        {
            Matrix rotationMatrix = Matrix.CreateRotationY(
                MathHelper.ToRadians(45.0f));
            // Create a vector pointing the direction the camera is facing.
            Vector3 transformedReference = Vector3.Transform(cameraReference,
                rotationMatrix);
            // Calculate the position the camera is looking at.
            cameraTarget = cameraPosition + transformedReference;
        }

        public void TransformVectorByReferenceAndOut()
        {
            Matrix rotationMatrix = Matrix.CreateRotationY(
                MathHelper.ToRadians(45.0f));
            // Create a vector pointing the direction the camera is facing.
            Vector3 transformedReference;
            Vector3.Transform(ref cameraReference, ref rotationMatrix,
                out transformedReference);
        }

        public void TransformVectorByReferenceAndOutVectorAdd()
        {
            Matrix rotationMatrix;
            Matrix.CreateRotationY(MathHelper.ToRadians(45.0f),
                out rotationMatrix);
            // Create a vector pointing the direction the camera is facing.
            Vector3 transformedReference;
            Vector3.Transform(ref cameraReference, ref rotationMatrix,
                out transformedReference);
            // Calculate the position the camera is looking at.
            Vector3.Add(ref cameraPosition, ref transformedReference,
                out cameraTarget);
        }

        public void InitializeTransformWithCalculation()
        {
            float aspectRatio = (float)640 / (float)480;
            projection = Matrix.CreatePerspectiveFieldOfView(
                MathHelper.ToRadians(45.0f), aspectRatio, 0.0001f, 1000.0f);
            view = Matrix.CreateLookAt(cameraPosition, cameraTarget, Vector3.Up);
        }

        public void InitializeTransformWithConstant()
        {
            float aspectRatio = (float)640 / (float)480;
            projection = Matrix.CreatePerspectiveFieldOfView(
                MathHelper.PiOver4, aspectRatio, 0.0001f, 1000.0f);
            view = Matrix.CreateLookAt(cameraPosition, cameraTarget, Vector3.Up);
        }

        public void InitializeTransformWithDivision()
        {
            float aspectRatio = (float)640 / (float)480;
            projection = Matrix.CreatePerspectiveFieldOfView(
                MathHelper.Pi / 4, aspectRatio, 0.0001f, 1000.0f);
            view = Matrix.CreateLookAt(cameraPosition, cameraTarget, Vector3.Up);
        }

        public void InitializeTransformWithConstantReferenceOut()
        {
            float aspectRatio = (float)640 / (float)480;
            Matrix.CreatePerspectiveFieldOfView(
                MathHelper.ToRadians(45.0f), aspectRatio, 0.0001f, 1000.0f,
                out projection);
            Matrix.CreateLookAt(
                ref cameraPosition, ref cameraTarget, ref vectorUp, out view);
        }

        public void InitializeTransformWithPreDeterminedAspectRatio()
        {
            Matrix.CreatePerspectiveFieldOfView(
                MathHelper.ToRadians(45.0f), 1.33333f, 0.0001f, 1000.0f,
                out projection);
            Matrix.CreateLookAt(
                ref cameraPosition, ref cameraTarget, ref vectorUp, out view);
        }

        public void CreateCameraReferenceWithProperty()
        {
            Vector3 cameraReference = Vector3.Forward;
            Matrix rotationMatrix;
            Matrix.CreateRotationY(
                MathHelper.ToRadians(45.0f), out rotationMatrix);
            // Create a vector pointing the direction the camera is facing.
            Vector3 transformedReference;
            Vector3.Transform(ref cameraReference, ref rotationMatrix,
                out transformedReference);
            // Calculate the position the camera is looking at.
            cameraTarget = cameraPosition + transformedReference;
        }

        public void CreateCameraReferenceWithValue()
        {
            Vector3 cameraReference = new Vector3(0, 0, -1.0f);
            Matrix rotationMatrix;
            Matrix.CreateRotationY(
                MathHelper.ToRadians(45.0f), out rotationMatrix);
            // Create a vector pointing the direction the camera is facing.
            Vector3 transformedReference;
            Vector3.Transform(ref cameraReference, ref rotationMatrix,
                out transformedReference);
            // Calculate the position the camera is looking at.
            cameraTarget = cameraPosition + transformedReference;
        }

        public void RotateWithoutMod()
        {
            cameraYaw += 2.0f;

            if (cameraYaw > 360)
                cameraYaw -= 360;
            if (cameraYaw < 0)
                cameraYaw += 360;

            float tmp = cameraYaw;
        }

        public void RotateWithMod()
        {
            cameraYaw += 2.0f;

            cameraYaw %= 360;

            float tmp = cameraYaw;
        }

        public void RotateElseIf()
        {
            cameraYaw += 2.0f;

            if (cameraYaw > 360)
                cameraYaw -= 360;
            else if (cameraYaw < 0)
                cameraYaw += 360;

            float tmp = cameraYaw;
        }
    }
}


  • Сохрани Решение (File -> Save All).

Назначение класса CheckPerformance - создание методов, которые выполняют одни и те же задачи разными способами. Сейчас неважно, что это за задачи. Суть в том, что необходимо понимать, что у нас есть различные группы методов, которые выполняют одни и те же задачи, но разными способами.
Рассмотрим три последних метода и убедимся, что все они делают одно и то же. Все три метода прибавляют число 2 к переменной cameraYaw и затем проверяют, что результат находится в пределах от 0 до 360. Идея проста. Здесь имитируется главный цикл программы, якобы читающий ввод с устройства ввода и обновляющий переменную cameraYaw в соответствии с введённыит данными.
А сейчас создадим консольное приложение, которое вызывает различные методы из библиотеки/класса CheckPerformance и замеряет количество времени, которое требуется на выполнение каждого из них.

Создаём консольное приложение

Приложение будет создано в том же Решении, что и библиотека (XNAPerformanceChecker).

  • В уже открытом Решении XNAPerformanceChecker, в правой части окна MSVC#2008 (Solution Explorer) щёлкни правой кнопкой мыши по верхнему узлу "Solution 'XNAPerformanceChecker'" и во всплывающем меню выбери Add -> New Project...
  • В появившемся окне New Project в разделе Project types укажи верхний раздел Visual C#. В правой части, в разделе Templates выбери Console Application. В поле Name укажи имя проекта XNAPerfStarter и жми OK (см. Рис.1).

В Solution Explorer появится второй проект. В левой части главного окна MS Visual C# 2008 откроется его файл исходного кода Program.cs .

  • Удали весь исходный код в Program.cs и замени его на следующий:
Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Diagnostics;

namespace XNAPerfStarter
{
class Program
{
    static int timesToLoop = 10000;

    static void Main(string[] args)
    {
        while (true)
        {
            XNAPerformanceChecker.CheckPerformance cp =
                new XNAPerformanceChecker.CheckPerformance();

            Stopwatch sw = new Stopwatch();

            //Call all methods once for any JIT-ing that needs to be done
            sw.Start();
            cp.InitializeTransformWithCalculation();
            cp.InitializeTransformWithConstant();
            cp.InitializeTransformWithDivision();
            cp.InitializeTransformWithConstantReferenceOut();
            cp.TransformVectorByValue();
            cp.TransformVectorByReferenceAndOut();
            cp.TransformVectorByReferenceAndOutVectorAdd();
            cp.CreateCameraReferenceWithProperty();
            cp.CreateCameraReferenceWithValue();
            sw.Stop();
            sw.Reset();

            int i;
            sw.Start();
            for (i = 0; i < timesToLoop; i++)
                cp.InitializeTransformWithCalculation();
            sw.Stop();

            PrintPerformance("         Calculation", ref sw);
            sw.Reset();

            sw.Start();
            for (i = 0; i < timesToLoop; i++)
                cp.InitializeTransformWithConstant();
            sw.Stop();

            PrintPerformance("            Constant", ref sw);
            sw.Reset();

            sw.Start();
            for (i = 0; i < timesToLoop; i++)
                cp.InitializeTransformWithDivision();
            sw.Stop();

            PrintPerformance("            Division", ref sw);
            sw.Reset();

            sw.Start();
            for (i = 0; i < timesToLoop; i++)
                cp.InitializeTransformWithConstantReferenceOut();
            sw.Stop();

            PrintPerformance("ConstantReferenceOut", ref sw);
            sw.Reset();

            sw.Start();
            for (i = 0; i < timesToLoop; i++)
                cp.InitializeTransformWithPreDeterminedAspectRatio();
            sw.Stop();

            PrintPerformance("         AspectRatio", ref sw);
            sw.Reset();

            Console.WriteLine();
            Console.WriteLine("——————————————————————");
            Console.WriteLine();

            sw.Start();
            for (i = 0; i < timesToLoop; i++)
                cp.TransformVectorByValue();
            sw.Stop();

            PrintPerformance("          Value", ref sw);
            sw.Reset();

            sw.Start();
            for (i = 0; i < timesToLoop; i++)
                cp.TransformVectorByReferenceAndOut();
            sw.Stop();

            PrintPerformance("ReferenceAndOut", ref sw);
            sw.Reset();

            sw.Start();
            for (i = 0; i < timesToLoop; i++)
                cp.TransformVectorByReferenceAndOutVectorAdd();
            sw.Stop();

            PrintPerformance("RefOutVectorAdd", ref sw);
            sw.Reset();

            Console.WriteLine();
            Console.WriteLine("——————————————————————");
            Console.WriteLine();

            sw.Start();
            for (i = 0; i < timesToLoop; i++)
                cp.CreateCameraReferenceWithProperty();
            sw.Stop();

            PrintPerformance("Property", ref sw);
            sw.Reset();

            sw.Start();
            for (i = 0; i < timesToLoop; i++)
                cp.CreateCameraReferenceWithValue();
            sw.Stop();

            PrintPerformance("   Value", ref sw);
            sw.Reset();

            Console.WriteLine();
            Console.WriteLine("——————————————————————");
            Console.WriteLine();

            sw.Start();
            for (i = 0; i < timesToLoop; i++)
                cp.RotateWithMod();
            sw.Stop();

            PrintPerformance("   RotateWithMod", ref sw);
            sw.Reset();

            sw.Start();
            for (i = 0; i < timesToLoop; i++)
                cp.RotateWithoutMod();
            sw.Stop();

            PrintPerformance("RotateWithoutMod", ref sw);
            sw.Reset();

            sw.Start();
            for (i = 0; i < timesToLoop; i++)
                cp.RotateElseIf();
            sw.Stop();

            PrintPerformance("    RotateElseIf", ref sw);
            sw.Reset();

            string command = Console.ReadLine();

            if (command.ToUpper().StartsWith("E") ||
                command.ToUpper().StartsWith("Q"))
                break;
        }
    }

    static void PrintPerformance(string label, ref Stopwatch sw)
    {
        Console.WriteLine(label + " – Avg: " +
            ((float)((float)(sw.Elapsed.Ticks * 100) /
            (float)timesToLoop)).ToString("F") +
            " Total: " + sw.Elapsed.TotalMilliseconds.ToString());
    }
}
}


  • Сохрани Решение (File -> Save All).

Два Проекта в Решении пока никак не связаны друг с другом. Для связи добавим в Проект консольного приложения XNAPerfStarter ссылку на Проект библиотеки (XNAPerfomanceChecker). Для этого:

  • В правой части окна MSVC#2008, в Solution Explorer, щёлкни правой кнопкой мыши по Проекту XNAPerfStarter. Во всплывающем контекстном меню выбери Add Reference....
  • В появившемся окне Add Reference выбери вкладку Projects и отметь единственный пункт в списке XNAPerformanceChecker. Жми ОК.

  • Сохрани Решение (File -> Save All).

Исследуем код Program.cs

В самом начале листинга видим определение using System.Diagnostics;
Фрагмент файла Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Diagnostics;
...

Класс System.Diagnostics необходим для доступа к функции Stopwatch, которую мы применяем для отслеживания количества времени, затраченного на выполнение каждого из методов. После старта отсчёта таймера мы выполняем главный цикл программы 100000 раз и вызываем метод класса CheckPerformance, созданный нами ранее. Как только цикл завершит своё выполнение определённое число раз, мы останавливаем Stopwatch-таймер и выводим результаты в консольное окно. Когда мы используем объект Stopwatch, перед началом каждого нового теста необходимо сперва вызвать метод Reset. Мы не стали встраивать вызов Reset в метод Stop на тот случай, если мы вдруг захотим просто поставить таймер на паузу (pause) и отмотать выполнение на несколько шагов назад. Мы также могли бы использовать метод static StartNew вместо экземпляра метода Start. Метод StartNew эффективно сбрасывает (reset) таймер, как только он возвращает новый экземпляр Stopwatch.
Когда замеряешь производительность (performance) куска кода, который выполняется очень быстро (даже внутри большого цикла), важно иметь возможность отследить, сколько именно времени понадобилось для его выполнения. Даже если счёт идёт на наносекунды.
Обычно замер времени в секундах не даёт нужной точности, поэтому здесь берём более мелкую единицу времени - милисекунду. В одной секунде 1000 милисекунд. Милисекунда, в свою очередь, состоит из 1000 микросекунд. Ещё меньше длится только тик, который и использует объект TimeSpan. 1 микросекунда = 10 тиков. Но и это не самая малая единица. Каждый состоит из 100 наносекунд. Таким образом 1 наносекунда - это 1 миллиардная доля секунды. См. таблицу:
Наносекунды Тики Микросекунды Милисекунды Секунды
100 1 0.1 0.0001 0.0000001
10 000 100 10 0.0100 0.00001
100 000 1000 100 0.1000 0.0001
1 000 000 10 000 1000 1.0000 0.001
10 000 000 100 000 10 000 10 0.01
100 000 000 1 000 000 100 000 100 0.1
1 000 000 000 10 000 1 000 000 1000 1


Источники:


1. Chad Carter. Microsoft XNA Game Studio 3.0 Unleashed. - Sams Publishing. 2009


ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Знакомство с XNA Framework и XNA Game Studio  »  XNA - Способы оптимизации

Contributors to this page: slymentat .
Последнее изменение страницы Среда 20 / Март, 2019 13:38:13 MSK автор slymentat.

Хостинг

Помочь проекту

Яндекс-деньги: 410011791055108