пятница, 18 сентября 2009 г.

четверг, 10 сентября 2009 г.

С#+XNA. В погоне за fps. I

Краткое содержание:

- Введение;
- Часть 1-я. Новичкам;
- Часть 2-я. Та же песня с Microsoft'ом;
- Часть 3-я философская. Охотник или жертва;
- Заключение.


Введение.

Данная статья является попыткой посмотреть в корень проблемы. Проблемы достижения высоких показателей fps с точки зрения построения C# кода не касаясь при этом XNA раздела 3D, относящегося к непосредственным инструкциям видеокарте.


Часть 1-я. Новичкам.

Мои наблюдения основаны на постоянном присутствии в различных форумах и участии в различных проектах. Соль рассматриваемого мною вопроса заключается в использовании Fields и Properties членов классов и структур.
Для полноты ощущений, в моих изысканиях, меня интересовали следующие ссылочные типы:
- class;
- interface;
- object.
Сейчас мы просмотрим на время выполнения кода при различной организации доступа к членам класса.
Пример:


using System;
namespace TestObjectInterface
{
class Program
{
static Unit[] unit;
static object[] unitobj;
static iUnit[] uniti;

static int count;

static void Main(string[] args)
{
count = 10000000;
unit = new Unit[count];
unitobj = new object[count];
uniti = new iUnit[count];
Init();
double tf = TestFields();
double tp = TestProperties();
double tfo = TestFieldsObject();
double tpo = TestPropertiesObject();
double tpi = TestPropertiesInterface();
Console.WriteLine("testFields = "
+ tf.ToString() + " mc, k = 1");
Console.WriteLine("testProperties = "
+ tp.ToString() + " mc, k = "
+ (tp / tf).ToString());
Console.WriteLine("testFieldsObject = "
+ tfo.ToString() + " mc, k = "
+ (tfo / tf).ToString());
Console.WriteLine("testPropertiesObject = "
+ tpo.ToString() + " mc, k = "
+ (tpo / tf).ToString());
Console.WriteLine("testPropertiesInterface = "
+ tpi.ToString() + " mc, k = "
+ (tpi / tf).ToString());
//Console.ReadKey();
}

static void Init()
{
for (int i = 0; i < count; i++)
{
unit[i] = new Unit(i, i * 2);
uniti[i] = (iUnit)unit[i];
unitobj[i] = (object)unit[i];
}
}

static double TestFields()
{
DateTime dtStart = DateTime.Now;
int n;
for (int i = 0; i < count; i++)
{
n = unit[i].x + unit[i].y; // 0
n = unit[i].x + unit[i].y; // 1
n = unit[i].x + unit[i].y; // 2
n = unit[i].x + unit[i].y; // 3
n = unit[i].x + unit[i].y; // 4
n = unit[i].x + unit[i].y; // 5
n = unit[i].x + unit[i].y; // 6
n = unit[i].x + unit[i].y; // 7
n = unit[i].x + unit[i].y; // 8
n = unit[i].x + unit[i].y; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}

static double TestProperties()
{
DateTime dtStart = DateTime.Now;
int n;
for (int i = 0; i < count; i++)
{
n = unit[i].X + unit[i].Y; // 0
n = unit[i].X + unit[i].Y; // 1
n = unit[i].X + unit[i].Y; // 2
n = unit[i].X + unit[i].Y; // 3
n = unit[i].X + unit[i].Y; // 4
n = unit[i].X + unit[i].Y; // 5
n = unit[i].X + unit[i].Y; // 6
n = unit[i].X + unit[i].Y; // 7
n = unit[i].X + unit[i].Y; // 8
n = unit[i].X + unit[i].Y; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}

static double TestFieldsObject()
{
DateTime dtStart = DateTime.Now;
int n;
for (int i = 0; i < count; i++)
{
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 0
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 1
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 2
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 3
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 4
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 5
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 6
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 7
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 8
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}

static double TestPropertiesObject()
{
DateTime dtStart = DateTime.Now;
int n;
for (int i = 0; i < count; i++)
{
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 0
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 1
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 2
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 3
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 4
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 5
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 6
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 7
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 8
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}

static double TestPropertiesInterface()
{
DateTime dtStart = DateTime.Now;
int n;
for (int i = 0; i < count; i++)
{
n = uniti[i].iX + uniti[i].iY; // 0
n = uniti[i].iX + uniti[i].iY; // 1
n = uniti[i].iX + uniti[i].iY; // 2
n = uniti[i].iX + uniti[i].iY; // 3
n = uniti[i].iX + uniti[i].iY; // 4
n = uniti[i].iX + uniti[i].iY; // 5
n = uniti[i].iX + uniti[i].iY; // 6
n = uniti[i].iX + uniti[i].iY; // 7
n = uniti[i].iX + uniti[i].iY; // 8
n = uniti[i].iX + uniti[i].iY; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}
}

interface iUnit
{
int iX {get;}
int iY {get;}
}

public class Unit : iUnit
{
public int x;
public int X { get { return x; } }
int iUnit.iX { get { return x; } }

public int y;
public int Y { get { return y; } }
int iUnit.iY { get { return y; } }

public Unit(int inX, int inY)
{
x = inX;
y = inY;
}
}
}

Я не буду уделять внимание коду, пример достаточно детский. Скажу только то, что по 10 инструкций в каждом цикле сделано для контрастного выделения времени выполнения тела цикла, от самой инструкции цикла.
У меня программа отработала следующим образом:


На мой взгляд все достаточно наглядно. Самый скоростной способ это естественно обращение к полю класса на прямую.
А теперь поясню к чему рассказал и продемонстрировал прописную истину. Всем так же известно, что в разработке 3D, в отличие от бизнес, приложений особенно ярко выражена погоня за скоростью. Но тем не менее, специалисты начинающие(!) работать с XNA, уже как правило, достаточно уверенно себя ощущают в области C# разработки. Вот в этом то и заключается основной казус. Который объясняется тем фактом, что вся справочная и обучающая литература пестрит примерами описывающими доступ к полям классов через свойства. В следствии чего ребята начинают автоматически(!) применять данный подход во всех случаях. И там где это действительно необходимо, и где не очень. Да это хороший тон(!). Удобно перехватывать обращения к полю класса, обрабатывать возникающие при этом исключительные ситуации. Но не подходит для применения в 3D(!!!).
Не всегда можно быстро выдать столько информации по данному вопросу очередному собеседнику. Да и сейчас я описал вопрос не за пять минут, но в дальнейшем могу с легкостью ссылаться на себя :).


Часть 2-я. Та же песня с Microsoft'ом.

Не для кого не секрет, что я люблю поковырять рефлектором все, что меня хотя бы мало-мальски интересует. Добавлю то, что порой это приносит больше информации, чем например чтение непосредственно MSDN. Поэтому в довесок к первой части я решил привести один(!) пример из библиотеки XNA.
Рассмотрим XNA 3.1, виндовую библиотеку Microsoft.Xna.Framework.dll,
одноименный namespace, и пусть подопытным будет структура Vector3.
Вот часть кода данной структуры, в рамках которой мы сейчас и пообщаемся:


public struct Vector3
{
public float X;
public float Y;
public float Z;
...
private static Vector3 _one;
public static Vector3 One
{
get
{
return _one;
}
}
...
public Vector3(float x, float y, float z)
{
this.X = x;
this.Y = y;
this.Z = z;
}
...
static Vector3()
{
...
_one = new Vector3(1f, 1f, 1f);
...
}
}

К полям X, Y и Z вопросов нет. Но обращаю ваше внимание на свойство One. По логике One возвращает единичный вектор постоянного значения. А вот тут по подробнее. Разум цепляется за формулировку "постоянное значение". Читаем MSDN и выясняем, что переменная или поле с постоянным значением может быть объявлена при помощи двух ключевых слов модификаторов доступа, которыми являются const и readonly. На всякий случай поясню формулировкой, переменная или поле постоянного значения бывает двух видов:
- const -> применяется при объявления полей и локальных переменных, постоянное значение присваивается на этапе компиляции, исключительно(!) при объявлении;
- readonly -> применяется при объявления полей, значение может присваивается как при объявлении, так и в конструкторе данного класса или структуры.
Но речь идет о поле постоянного значения которое не возможно рассчитать при компиляции, что накладывает ряд ограничений и в соответствии с рассматриваемым нами случаем, расширим формулировки:
- Модификатор static не допускается в объявлении константы;
- значение константы должно быть полностью вычислено во время компиляции;
- Единственными возможными значениями для констант ссылочных типов являются string и null;
Ясное дело на static ставим крест, он однозначно не подходит. А вот readonly наш размерчик! И как нельзя лучше вписывается в нашу ситуацию. Потому, заранее уже зная куда нас это приведет, предлагаю по тестировать и сравнить время чтения значения из свойства стандартной структуры с чтением значения из иначе построенной структуры.
Смотрим пример:


using System;
using Microsoft.Xna.Framework;
namespace TestFields
{
class Program
{

static int count;

static void Main(string[] args)
{
count = 10000000;
Console.WriteLine("testStandartVector3 = " +
TestStandartVector3().ToString() +
" mc");
Console.WriteLine("testMyVector3 = " +
TestMyVector3().ToString() +
" mc");
}

static double TestStandartVector3()
{
DateTime dtStart = DateTime.Now;
Vector3 vec;
for (int i = 0; i < count; i++)
{
vec = Vector3.One; // 0
vec = Vector3.One; // 1
vec = Vector3.One; // 2
vec = Vector3.One; // 3
vec = Vector3.One; // 4
vec = Vector3.One; // 5
vec = Vector3.One; // 6
vec = Vector3.One; // 7
vec = Vector3.One; // 8
vec = Vector3.One; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}

static double TestMyVector3()
{
DateTime dtStart = DateTime.Now;
gVector3 vec;
for (int i = 0; i < count; i++)
{
vec = gVector3.One; // 0
vec = gVector3.One; // 1
vec = gVector3.One; // 2
vec = gVector3.One; // 3
vec = gVector3.One; // 4
vec = gVector3.One; // 5
vec = gVector3.One; // 6
vec = gVector3.One; // 7
vec = gVector3.One; // 8
vec = gVector3.One; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}
}

public struct gVector3
{
public float X;
public float Y;
public float Z;

public static readonly gVector3 One;

public gVector3(float x, float y, float z)
{
X = x;
Y = y;
Z = z;
}

static gVector3()
{
One = new gVector3(1, 1, 1);
}
}
}

Получаем вот такой вот результат:


Внимание вопрос(!): какая была необходимость реализовывать "постоянное значение" через свойство возвращающее значение приватного поля, если в сравнительном отношении скорости, с реализацией через модификатор доступа readonly, составляет разницу практически в(!) два раза? Мне не понятно, а вам? Буду очень признателен тому человеку, который вежливо мне объяснит.
Товарищам, которые планируют достаточно часто применять постоянные вектора:
- Zero;
- One;
- UnitX;
- UnitY;
- UnitZ;
- Up;
- Down;
- Right;
- Left;
- Forward;
- Backward.
Рекомендую не использовать стандартные, а реализовать самостоятельно, как в выше приведенном примере.
Мысль которую я хотел донести, звучит следующим образом:
- изучайте все средства, которые собираетесь применять в своих проектах;
- в результате изучения берите только лучшее и делайте наконец то сказку былью;
- не знание законов не освобождает от ответственности. Это про специалистов, которые по своей беспечности, не разбираясь, применяют все подряд, а в последствии сетуют на Microsoft. Хотя это говорит с плохой стороны не(!) о Microsoft.
- допуская то, что не смотря на "много букв", у читателей данной статьи мнения разойдутся. Я добавил третью часть. Читать рекомендую всем, только одним принять к сведению, а иным просто улыбнуться.


Часть 3-я философская. Охотник или жертва?

Жертвам своей не компетентности, которые любят плохо отзываться о Microsoft, посвящается.
Что, из себя, представляет понятие мода? Мода - в широком смысле этого слова, определяет стремление, того или иного человека, быть похожим на основное большинство. Хм, так то оно так, когда в меру, без фанатизма и крайностей. А так же не идет в разрез с законами эволюции, которые не зависимо от нас хранят приоритетное право "сильных" и "особенно удачных" особей выживать. К примеру обратная крайность это не формальные движения в нашем обществе.
Применяем выше описанное отступление к нашей основной теме. И так у нас есть три основных типа людей. Систематизируем их и результат оформим в виде структуры:
- ярко выражено не(!) довольные Microsoft'ом -> модные "жертвы". Да, уже более десяти лет считается модно и наверное круто так себя вести;
- фанаты Microsoft'а -> назовем их "панки", так веселее смотрится не формальное определение. Обычно эти люди либо являются специалистами, но узкого профиля, либо совсем не являются таковыми. Если кого то обидел, сообщите мне. Я подберу более мягкое определение.
- специалисты -> "охотники". Ни кому не покланяются, ни кого не ругают. Им некогда отвлекаться на разную чушь. Эти люди берут от жизни только лучшее, прекрасно понимая при этом как и где это лучшее применять, и как и где не стоит. Они же становятся MCP, MVP, "черными поясами" Intel, … , и другими почетными товарищами.


Заключение.

Ну вот и все. Всем перца насыпал. Развеялся. Можно дальше "охотиться" ... оговорился :) ... работать и точить свое мастерство.