В предыдущем уроке мы узнали, как заполнять наши треугольники. Поскольку мы работаем на базе процессора с нашим программным 3D-движком, это действительно начинает стоить много процессорного времени. Хорошей новостью является то, что современные процессоры являются многоядерными . Тогда мы могли бы представить себе использование параллелизма для повышения производительности . Мы собираемся сделать это только в C #, и я объясню, почему мы не будем делать это в HTML5. Мы также увидим несколько простых советов, которые могут повысить производительность в таком коде цикла рендеринга. В самом деле, мы собираемся перейти с 5 FPS на 50 FPS , что увеличит производительность в 10 раз!
Вычислить FPS
Первым шагом является вычисление FPS, чтобы можно было проверить, собираемся ли мы повысить производительность, изменив наш алгоритм. Вы можете сделать это в C # или TypeScript / JavaScript, конечно.
Нам нужно знать время дельты между двумя кадрами . Затем нам просто нужно захватить текущее время, нарисовать новый кадр ( requestAnimationFrame в HTML5 или CompositionTarget.Rendering в XAML), снова захватить текущее время и сравнить его с предыдущим сохраненным временем. Вы получите результат в миллисекундах. Чтобы получить FPS, просто разделите 1000 на этот результат. Например, если это 16,66 мс, оптимальное время дельты , у вас будет 60 FPS.
Вы можете сделать это после каждого рендеринга кадра, чтобы иметь очень точный FPS или вычислить средний FPS, например, для 60 выборок. Мы с Дэвидом уже работали над этой темой в этой серии: Сравнительный анализ игры HTML5: HTML5 Potatoes Gaming Bench
В заключение, в C # добавьте новый элемент управления TextBlock XAML с именем « fps » и используйте этот код для вычисления FPS:
DateTime previousDate; void CompositionTarget_Rendering ( отправитель объекта , объект e) // Fps var now = DateTime .Now; var currentFps = 1000.0 / (сейчас - previousDate) .TotalMilliseconds; previousDate = сейчас; fps.Text = string .Format ( "{0: 0.00} fps" , currentFps); // цикл рендеринга device.Clear (0, 0, 0, 255); foreach ( сетка в ячейках) { mesh.Rotation = new Vector3 (mesh.Rotation.X, mesh.Rotation.Y + 0.01f, mesh.Rotation.Z); device.Render (мера, сетка); } device.Present ();
Используя этот код, используя собственное разрешение моего Lenovo Carbon X1 Touch (1600 × 900), я использую в среднем 5 FPS с решением C #, описанным в предыдущей статье. Мой Lenovo встраивает Intel Core i7-3667U с графическим процессором HD4000. Это гиперпоточный двухъядерный процессор. Затем он показывает 4 логических процессора .
Стратегии оптимизации и распараллеливания
Приложения WinRT используют .NET Framework 4.5, которая по умолчанию включает библиотеку параллельных задач (TPL). Если вы обращаете внимание на то, как вы пишете свой алгоритм, и если ваш алгоритм может быть распараллелен, сделать его параллельным становится очень легко благодаря TPL. Если вы еще не знаете эту концепцию, обратите внимание на Параллельное программирование в .NET Framework 4: Начало работы
Избегайте прикосновений к элементам управления пользовательского интерфейса
Первое правило для многопоточности / многозадачности состоит в том, чтобы код 0 касался интерфейса в порождении потоков. Только поток пользовательского интерфейса может касаться / манипулировать графическими элементами управления. В нашем случае у нас был фрагмент кода, обращающийся к bmp.PixelWidth или bmp.PixelHeight, где bmp имеет тип WriteableBitmap . WriteableBitmap рассматривается как элемент пользовательского интерфейса и не является потокобезопасным. Вот почему нам нужно сначала изменить эти блоки кода, чтобы сделать их «параллелизуемыми». В предыдущем уроке мы начали с этого. Вам просто нужно сохранить эти значения в начале. Мы сделали это в renderWidth и renderHeight . Используйте эти значения в своем коде вместо доступа к bmp . Измените все ссылки на bmp.PixelWidth на renderWidth и на bmp.PixelHeight на renderHeight.
Кстати, это правило важно не только для распараллеливания. Это также для оптимизации производительности в целом. Таким образом, просто удаляя доступ к свойствам WriteableBitmap в моем коде, я переключаюсь со средней скорости 5 FPS на более чем 45 FPS на одной машине!
Это же правило очень важно (даже, может быть, даже больше) в HTML5. Вы должны избегать прямого тестирования свойств элементов DOM . Операции DOM очень медленные. Так что на самом деле не стоит заходить к ним каждые 16 мсек, если в этом нет необходимости. Всегда кэшируйте значения, которые необходимо проверить позже. Мы уже делали это в предыдущих уроках для HTML5-версии 3D-движка.
Быть самодостаточным
Второе правило заключается в том, что блок кода, который будет запущен на нескольких доступных ядрах, должен быть самодостаточным . Вашему коду не нужно слишком долго ждать, пока результат другого блока кода продолжится, иначе это снизит интерес к параллелизму. В нашем случае вам повезло, так как мне удалось дать вам код в предыдущих уроках, чтобы уже соблюдать это правило.
Возможно, вы видели, что у нас есть несколько областей, где мы можем переключить наш классический цикл For с помощью цикла Parallel.For .
Первый случай в методе DrawTriangle . Затем мы собираемся нарисовать несколько линий на треугольнике параллельно. Затем вы можете легко преобразовать цикл 2 normal For в цикл 2 Parallel.For :
если (dP1P2> dP1P3) Параллельно .For (( int ) p1.Y, ( int ) p3.Y + 1, y => { если (у <p2.Y) { ProcessScanLine (y, p1, p3, p1, p2, color); } еще { ProcessScanLine (y, p1, p3, p2, p3, color); } }); еще { Параллельно .For (( int ) p1.Y, ( int ) p3.Y + 1, y => { если (у <p2.Y) { ProcessScanLine (y, p1, p2, p1, p3, color); } еще { ProcessScanLine (y, p2, p3, p1, p3, color); } });
Но в моем случае результат немного удивителен. Я понижаю производительность, переключаясь с 45 FPS до 40 FPS ! Так что может быть причиной этого недостатка производительности?
Что ж, в этом случае параллельное рисование нескольких линий недостаточно для питания ядер. Затем мы тратим больше времени на переключение контекста и переход от одного ядра к другому, чем на реальную обработку. Это можно проверить с помощью встроенных инструментов профилирования Visual Studio 2012: Concurrency Visualizer для Visual Studio 2012
Вот карта использования ядер с этим первым подходом распараллеливания :
Различные цвета связаны с рабочими потоками. Это действительно неэффективно. Посмотрите на разницу с непараллельной версией :
У нас работает только один поток (зеленый), который отправляется на несколько ядер самой ОС. Даже если в этом случае мы не будем использовать возможности многоядерных процессоров, мы, наконец, станем более эффективными в глобальном масштабе. В нашем первом подходе к распараллеливанию мы генерируем слишком много переключений.
Защитите с помощью блокировки и выберите правильный цикл для распараллеливания
Ну, я полагаю, вы пришли к тому же выводу, что и я. Распараллеливание циклов в методе drawTriangle не кажется хорошим вариантом. Нам нужно найти что-то, что займет больше времени для выполнения и будет более эффективным при переключении ядер. Вместо того, чтобы рисовать треугольник параллельно, мы собираемся нарисовать несколько треугольников параллельно . В заключение, каждое ядро будет обрабатывать рисунок полного треугольника.
Проблема с использованием этого подхода заключается в методе PutPixel . Теперь, когда мы хотим нарисовать несколько граней параллельно, мы можем попасть в случаи, когда 2 ядра / нити будут пытаться получить доступ к одному и тому же пикселю в параллельном режиме. Затем нам нужно защитить доступ к пикселю, прежде чем работать с ним. Нам также необходимо найти эффективный способ блокировки доступа к пиксельным буферам. Действительно, если мы тратим больше времени на защиту данных, чем на работу с ними, распараллеливание снова станет бесполезным.
Решение состоит в том, чтобы использовать массив, содержащий поддельные объекты, которые мы заблокируем.
закрытый объект [] lockBuffer; Публичное устройство ( WriteableBitmap BMP) это .bmp = bmp; renderWidth = bmp.PixelWidth; renderHeight = bmp.PixelHeight; // размер заднего буфера равен количеству пикселей для рисования // на экране (ширина * высота) * 4 (значения R, G, B и Alpha). backBuffer = новый байт [renderWidth * renderHeight * 4]; deepBuffer = new float [renderWidth * renderHeight]; lockBuffer = новый объект [renderWidth * renderHeight]; for ( var i = 0; i <lockBuffer.Length; i ++) { lockBuffer [i] = новый объект (); } // Вызывается поставить пиксель на экран с определенными координатами X, Y public void PutPixel ( int x, int y, float z, Color4 color) // Поскольку у нас есть 1-D массив для нашего заднего буфера // нам нужно знать эквивалентную ячейку в 1-D // в 2D координатах на экране var index = (x + y * renderWidth); var index4 = index * 4; // Защита нашего буфера от параллелизма потоков блокировка (lockBuffer [индекс]) { if (deepBuffer [index] <z) { возврат ; // Сброс } deepBuffer [index] = z; backBuffer [index4] = ( byte ) (color.Blue * 255); backBuffer [index4 + 1] = ( байт ) (color.Green * 255); backBuffer [index4 + 2] = ( байт ) (color.Red * 255); backBuffer [index4 + 3] = ( байт ) (color.Alpha * 255); }
Используя этот второй подход, я двигаюсь в среднем с 45 FPS до 53 FPS. Вы можете подумать, что прирост производительности не так впечатляет. Но в следующем уроке метод drawTriangle будет намного сложнее обрабатывать тени и освещение. Например, используя этот подход с затенением Гуро, распараллеливание почти удвоит производительность .
Мы также можем проанализировать новое представление Cores с помощью этого второго подхода распараллеливания:
Сравните его с предыдущим представлением Cores, и вы поймете, почему это намного эффективнее.
Вы можете скачать решение C #, содержащее эту оптимизированную версию, здесь:
— C # : SoftEngineCSharpPart4Bonus.zip
Итак, что не так с HTML5 / JavaScript в этом случае?
HTML5 предлагает новый API для разработчиков JavaScript для обработки аналогичных подходов. Он называется Web Workers и может решать проблему использования многоядерных процессоров в определенных сценариях.
Дэвид Катухе и я уже освещали эту тему несколько раз в этих 3 статьях:
— Введение в веб-работников HTML5: многопоточный подход JavaScript : сначала прочитайте эту статью, если вы еще не знаете веб-работников
— Использование веб-работников для повышения производительности манипулирования изображениями : очень интересная статья, в которой мы работаем над работой с пикселями.
— Серия учебных пособий : использование WinJS и WinRT для создания забавного приложения камеры HTML5 для Windows 8 (4/4) : четвертая часть серии учебных пособий, в которой я использую веб-работников для применения некоторых фильтров к изображениям, снятым с помощью веб- камеры .
Общение с работниками осуществляется через сообщения. Это означает, что большую часть времени данные отправляются работникам в виде копии из потока пользовательского интерфейса. Очень немногие типы отправляются по ссылке. Если вы, кстати, являетесь разработчиком на C ++, не воспринимайте это как ссылку. Действительно, в переносимых объектах исходный объект очищается из контекста вызывающего (поток пользовательского интерфейса) при передаче работнику. И в любом случае сегодня почти только ArrayBuffer попадает в эту категорию, и нам скорее нужно будет отправить тип ImageData .
Но это не наша главная проблема, чтобы попытаться ускорить наш 3D-движок в HTML5. В случае копирования данные отправляются с помощью операции memcpy (), которая выполняется очень быстро. Реальная проблема заключается в том, когда рабочие закончат свою работу по обработке. Вам необходимо отправить результаты обратно в основной поток, и этот поток пользовательского интерфейса должен перебирать все массивы, отправленные обратно, чтобы перестроить массивы основных пикселей. Эта операция, к сожалению, просто убьет любой прирост производительности, который мы могли бы получить у рабочих.
В заключение, я не нашел способа реализовать подход параллелизма в HTML5 с нашим программным механизмом 3D. Но я, возможно, что-то пропустил. Если вам удастся обойти текущие ограничения веб-работников для получения значительного прироста производительности, я открыт для предложений! 🙂
В нашем следующем уроке мы вернемся к регулярным урокам, чтобы поговорить о плоской штриховке и штриховке Гуро. Наши объекты начнут действительно сиять! 🙂
Первоначально опубликовано: http://blogs.msdn.com/b/davrous/archive/2013/06/25/tutorial-part-4-bonus-learning-how-to-write-a-3d-software-engine-in -c-ts-or-js-optimizing-amp-parallelism.aspx . Перепечатано здесь с разрешения автора.