Опубликован: 15.05.2013 | Доступ: свободный | Студентов: 265 / 9 | Длительность: 24:25:00
Специальности: Системный архитектор
Лекция 13:

Компоненты WinRT: введение

< Лекция 12 || Лекция 13: 123 || Лекция 14 >

Краткое руководство №2: создание компонента в C++

Для того, чтобы продемонстрировать работу с WinRT-компонентами, написанными на C++, я так же добавил проект в упражнение "Image Manipulation example", назвав его PixelCruncherCPP. Основной код там такой же самый, как и в примере на C# - процедура манипуляции с пикселями довольно универсальна! Необходимая структура кода компонента, с другой стороны, уникальна для языка: у C++ есть собственные особенности.

Как мы поступали с C#, давайте добавим новый проект, использовав шаблон Visual C++ > Компонент среды выполнения (Visual C++ > Windows Runtime Component) и имя PixelCruncherCPP. После переименования Class1 в Tests в коде и переименования файла, мы видим следующий код в заголовочном файле (я назвал его grayscale.h, и опустил директивы компилятора):

namespace PixelCruncherCPP
{
public ref class Tests sealed
{
public:
Tests();
};
}

Здесь мы видим, что класс должен быть объявлен public ref и sealed, с общедоступным конструктором. Всё это вместе даёт возможность создать экземпляр объекта в качестве WinRT-компонента. В Tests.cpp мы видим следующее:

#include "pch.h"
#include "Grayscale.h"

using namespace PixelCruncherCPP;
using namespace Platform;

Tests::Tests()
{
}

Опять же, не так уж и много всего, но вполне достаточно (Документация по пространству имен Platform (http://msdn.microsoft.com/library/windows/apps/hh710417.aspx), кстати, это часть Справочника по языку Visual C++.) Для того, чтобы реализовать то же самое, что мы сделали для C++, добавим статический тестовый метод и соответствующее свойство. Определение класса выглядит так:

public ref class Tests sealed
{
public: Tests();

static Platform::String^ TestMethod(bool throwAnException);
property int TestProperty;
};

А вот код для TestMethod:
String^ Tests::TestMethod(bool throwAnException)
{
if (throwAnException)
{
throw ref new InvalidArgumentException;
}
return ref new String(L"Tests.TestMethod succeeded");
}

Когда вы выполните построение этого проекта (Построение > Построить проект (Build > Build Solution)), вы увидите, что теперь у нас имеются файлы PixelCruncherCPP.dll и PixelCruncherCPP.winmd. В то время, как C#-сборка может содержать и код и метаданные, C++-компонент компилируется в виде отдельных файлов для кода и метаданных. Эти метаданные, опять же, используются для проецирования ABI компонента в другие языки и предоставления данных IntelliSense Visual Studio и Blend. Если теперь вы добавите ссылку на этот компонент в проекте приложения, щёлкнув правой кнопкой по проекту, выбрав Добавить ссылку > Решение (Add Reference > Solution) и затем выбрав PixelCruncherCPP, как на рис. 13.2., вы обнаружите, что IntelliSense работает с классом, когда вы пишете JavaScript код.

Так же вы можете обнаружить соответствующее приведение имен свойств и методов к виду, принятому в JavaScript. На самом деле, за исключением пространства имен, PixelCruncherCPP, всё, что мы делали, используя C#-компонент в JavaScript, выглядит точно так же, как и должно: приложение, использующее возможности WinRT-компонента не должно беспокоиться о языке, который использован для реализации этого компонента. И отладка, более того, выглядит точно так же, за исключением того, что вам нужно выбрать тип отладки Только машинный код или Смешанный (управляемый и машинный) (Native Only или Mixed (Managed And Native)) в диалоговом окне, показанном ранее на рис. 13.3.

Теперь нам нужно сделать то же самое для того, чтобы использовать массивы в компоненте, в качестве справки посмотрите материал "Классы Array и WriteOnlyArray" (http://msdn.microsoft.com/library/windows/apps/hh700131.aspx). В C++ входной массив объявляется с помощью Platform::Array<T> ^, а выходной – Platform::WriteOnlyArray<T> ^, где мы используем в качестве типа uint8 вместо типа Byte in C#:

   
  bool Grayscale::Convert(Platform::Array<uint8>
       ^ imageDataIn,
    Platform::WriteOnlyArray<uint8>^ imageDataOut)

Остаток кода точно такой же, как раньше, за исключением этого изменения типа и того, как мы получаем длину входного массива, поэтому нам не нужно его здесь приводить. Код для вызова этого класса из JavaScript тоже такой же точно, как для C++.

var pc2 = new PixelCruncherCPP.Grayscale();
pc2.convert(pixels, imgData.data);
Врезка: Библиотека шаблонов C++ среды выполнения Windows (WRL)

Visual Studio включает в себя то, что называется Библиотека шаблонов C++ среды выполнения (Windows Runtime C++ Template Library или WRL) (http://msdn.microsoft.com/library/hh438466%28v=vs.110%29.aspx), которая поможет вам писать низкоуровневые WinRT-компоненты на C++. Это настоящий посредник между низкоуровневым COM и тем, что называется расширениями компонентов C++/CX, которые мы, на самом деле, и используем в этом разделе. Если у вас есть некоторый опыт работы с Active Template Library (ATL) для COM, вы будете чувствовать себя как дома с WRL. Для того, чтобы узнать подробности, посмотрите вышеупомянутую документацию и пример "WinRT-компонент с использованием WRL" (http://code.msdn.microsoft.com/windowsapps/Windows-Runtime-Component-e3e1e38d).

Сравнение результатов

Упражнение к этой главе "Image Manipulation", материалы которого содержатся в дополнительных материалах, содержит эквивалентный код на JavaScript, C#, и C++ для выполнения конвертации пикселей изображеня в оттенки серого. Выполняя замеры времени с помощью данных, полученных от new Date()в коде каждой процедуры, я составил таблицу показателей производительности, приведенную ниже .

Среднее время исполнения, в миллисекундах (пять образцов; двухьядерный 2.5GHz процессор)
Размер изображения JavaScript C# C++
14.8K 8.4 7.2 6.4
231K 45.2 40 33.8
656K 76.6 65.8 54.4
1.98MB 798 728 598
4.57MB 796 750 637

Несколько замечаний и наблюдений об этих показателях и их измерении:

  • Выполняя тестирование производительности, наподобие этого, не забудьте установить цель построения в Release (Развертывание) вместо Debug (Отладка). Это приведет к значительной разнице в производительности C++-кода, так как компилятор добавляет множество дополнительных механизмов при построении проекта для отладочных целей.
  • Выполняя измерения, удостоверьтесь в том, что запускаете приложение, предназначенное для развертывания вне отладчика (в Visual Studio выберите команду Отладка > Запуск без отладки (Debug > Start Without Debugging)). Если вы включили отладку скрипта, JavaScript-код будет работать заметно медленнее, чем в варианте построения для развертывания, что может привести к неправильному впечатлению того, что этот язык гораздо менее эффективен, чем на самом деле.
  • Если вы выполняете похожие тесты самостоятельно, вы можете отметить, что время, замеренное для операции конвертации гораздо меньше, чем время, когда приложение снова способно реагировать на действия пользователя. Это потому, что исполнение метода putImageData элемента canvas занимает довольно много времени для копирования конвертированных пикселей. На самом деле, основное количество всего процесса занимает именно работа putImageData, а не процесс конверсии в оттенки серого.
  • Если предположить, что нагрузка на процессор для конверсии в оттенки серого примерно одинакова для разных реализаций, вы можете видеть, что более высокопроизводительные компоненты уменьшают время, в течение которого процессор подвергается подобной нагрузке. При множестве вызовов подобных процедур, в итоге, высокопроизводительные компоненты значительно способствуют делу экономии электроэнергии.
  • При первом использовании WinRT – компонента для любой задачи, немного больше времени уходит на загрузку компонента и его метаданных. Вышеприведенные значения не включают измерение времени при первом запуске. Таким образом, если вы хотите оптимизировать процесс загрузки, эта дополнительная нагрузка может означать, что лучше выполнить все операции, используя JavaScript.

Опираясь на эти данные, мы можем увидеть, что код на C# исполняется на 6–21% быстрее, чем эквивалентный JavaScript, а C++ на 25–46% быстрее. C++, кроме того, на 13–22% быстрее, чем C#. Это означает, что для некритичного кода написание компонента необязательно даст вам результат, достойный потраченного времени. Более продуктивным будет сделать это средствами JavaScript. Но использование компонентов там, где производительность действительно важна, безусловно, принесет замечательные результаты.

Совет. Есть гораздо больше способов для исследования и повышения производительности приложений, чем просто вынос интенсивных вычислительных задач в WinRT-компоненты. Материалы "Анализ производительности приложений для Магазина Windows" (http://msdn.microsoft.com/library/windows/apps/hh696636) и "Анализ качества кода приложений для Магазина Windows с помощью средств анализа кода Visual Studio" (http://msdn.microsoft.com/library/windows/apps/hh441471) позволят вам произвести более глубокую оценку вашего приложения.

Мне так же хочется добавить, что когда я впервые запускал вышеописанные тесты, я иногда видел примерно 100% превосходство в производительности C#/C++ над JavaScript. Причина этого заключается, скорее, в особенностях объекта ImageData элемента canvas (возвращаемого методом createImageData этого элемента), чем в самом JavaScript. В исходном JavaScript коде (с тех пор, как он был исправлен в Главе 4 курса "Пользовательский интерфейс приложений для Windows 8, созданных с использованием HTML, CSS и JavaScript"), я обращался к элементам данных массива ImageData.data array для установки каждого значения r, g, b, и a каждоо пикселя. Когда я понял, как подобные обращения замедляют код, я поменял код, добавил кэширование массива в другой переменной и неожиданно JavaScript-версия стала работать гораздо быстрее. На самом деле, уменьшение использования ссылок на идентификаторы – это обычно хорошая практика повышения производительности в JavaScript. Для того, чтобы узнать больше об этом и других аспектах производительности, обратитесь к книге "High Performance JavaScript", которую написал Nicholas C. Zakas (O’Reilly, 2010).

Врезка: Управляемый код против машинного

Как показано в предыдущем разделе, переход от JavaScript к C# даёт вам первый уровень улучшения производительности, а переход от C# к C++ добавляет еще один. Учитывая то, что работа с C++ обычно сложнее, полезно задаться вопросом, стоит ли вкладывать в это дополнительные усилия. В особенно критических ситуациях, где дополнительные 13–22% производительности имеют реальное значение, ответ очевиден: стоит. Но есть и другие факторы для рассмотрения: разница между управляемой средой исполнения .NET-языков (вместе с JavaScript, если на то пошло) и машинной средой исполнения C++.

Проще говоря, причина, по которой код на C#/VB чаще легче писать, чем код на C++ заключается в том, что .NET Common Language Runtime (CLR) предоставляет множество служб, наподобие сборки мусора, в итоге вы не должны беспокоиться о каждом небольшом выделении памяти. Что это означает, однако, что ваши действия в C#/VB так же могут вызывать выполнение дополнительных задач в среде исполнения, что может изменить характеристики производительности компонентов.

Например, в упражнении "Image Manipulation" к этой главе, которое я по-настоящему расширил до приложения для тестирования компонентов, я добавил простую функцию, выполняющую сложение чисел в JavaScript, C#, и C++ (они все выглядят примерно одинаково):

function countFromZero(max, increment) {
var sum = 0;

  for (var x = 0; x < max; x += increment) {
sum += x;
}
return sum;
}

Выполняя подсчет с заданным максимумом (max) в 1000 и приращением в 0.000001 (используйте подобное приращение вне отладчика, иначе вам придется ждать некоторое время), я получил следующие средние значения измерения времени исполнения функции: 2112 мс для JavaScript, 1568 мс для C#, и 1534 мс для C++. Снова мы видим, что различие между JavaScript и другими языками весьма значительно (прирост в 35–38%), но оно не так уж и отличается при сравнении C# и C++.

Однако, я иногда обнаруживал, что после загрузки некоторого количества изображений и выполнения тестирования перевода их в оттенки серого, которое происходило в JavaScript и/или C#, эта операция могла занять гораздо больше времени, чем раньше, вероятнее всего из-за сборки мусора, которая действовала на производительность среды исполнения JavaScript и CLR. В C++ подобного не происходило, хотя, конечно, высокая нагрузка на процессор в любом случае замедляет любые действия.

Однако, обратите внимание, что я говорю о том, что такое происходит лишь иногда. Вам следует знать об этом, но не стоит беспокоиться об этом по любому поводу, исключая особенно критичные к производительности участки кода.

< Лекция 12 || Лекция 13: 123 || Лекция 14 >