Гмурнете се дълбоко в WPF системата за изобразяване
Първоначално не исках да публикувам тази статия. Струваше ми се, че е неучтиво да се говори за мъртвите или добре, или нищо. Но няколко разговора с хора, чието мнение наистина ценя, ме накараха да променя решението си. Разработчиците, които са инвестирали много усилия в платформата на Microsoft, трябва да са наясно с вътрешните характеристики на нейната работа, така че след като стигнат до задънена улица, да разберат причините за случилото се и по-точно да формулират желанията към разработчиците на платформата. Мисля, че WPF и Silverlight са добри технологии, но... Ако сте следили моя Twitter през последните няколко месеца, някои от коментарите може да изглеждат като неоснователни атаки срещу производителността на WPF и Silverlight. Защо написах това? В крайна сметка инвестирах хиляди и хиляди часове от собственото си време през годините в популяризиране на платформата, разработване на библиотеки, подпомагане на членове на общността и т.н. Определено се интересувам лично. Искам платформата да стане по-добра.

Когато проектирате пристрастяващ потребителски интерфейс, производителността е най-важна за вас. Без него всичко останало е безсмислено. Колко пъти ви се е налагало да опростявате интерфейс, защото е изоставал? Колко пъти сте измисляли „нов, революционен модел на потребителски интерфейс“, който е трябвало да бъде изхвърлен в кошчето, защото наличната технология не е позволявала да бъде внедрен? Колко пъти сте казвали на клиентите, че имате нужда от 2,4 GHz четириядрен процесор, за да работите добре? Клиентите многократно са ме питали защо не могат да получат същото гладко изживяване с WPF и Sliverlight, както биха направили с приложение за iPad, дори на четири пъти по-мощен компютър. Тези технологии могатса подходящи за бизнес приложения, но очевидно не са подходящи за потребителски приложения от следващо поколение.
Но WPF използва хардуерно ускорение. Защо смятате, че е неефективно?
WPF наистина използва хардуерно ускорение и част от вътрешната му реализация е много добре направена. За съжаление, ефективността на използване на GPU е много по-ниска, отколкото би могла да бъде. WPF системата за изобразяване използва много груба сила. Надявам се да обясня това твърдение по-долу.
Анализиране на единичен WPF рендиращ пропуск
За да анализираме ефективността, трябва да разберем какво всъщност се случва вътре в WPF. За това използвах PIX, програмата за профилиране на Direct3D, която идва с DirectX SDK. PIX стартира вашето D3D приложение и инжектира набор от прехващачи във всички Direct3D повиквания, за да ги анализира и наблюдава.
Създадох просто WPF приложение, което рисува две елипси отляво надясно. И двете елипси са с един и същи цвят (#55F4F4F5) с черен контур.

И как WPF изобразява това?
Първо, WPF изчиства (#ff000000) мръсната област, която е на път да преначертае. Необходими са мръсни зони, за да се намали броят на пикселите, изпратени до последния етап на сливане в конвейера на GPU. Можем дори да предположим, че това намалява количеството геометрия, която ще трябва да бъде повторно теселирана, повече за това след малко. След почистване на мръсния участък нашата рамка изглежда така

След това WPF прави нещо странно. Първо запълва буфера на върха и след това рисува нещо, което прилича на правоъгълник върху мръсната област. Сега рамката изглежда така (вълнуващо, нали?):

След това теселираелипса на GPU. Теселацията, както може би вече знаете, превръща геометрията на нашата елипса 100x100 в набор от триъгълници. Това се прави поради следните причини: 1) триъгълниците са естествена рендираща единица за GPU 2) теселирането на елипса може да доведе до само няколкостотин триъгълника, което е много по-бързо от растеризирането на 10 000 пиксела с CPU anti-aliasing (което прави Silverlight). Екранната снимка по-долу показва как изглежда теселацията. Читателите, запознати с 3D графиките, може би са забелязали, че това са триъгълни ивици. Имайте предвид, че елипсата изглежда незавършена в теселацията. Като следваща стъпка WPF взема теселацията, зарежда я във върховия буфер на графичния процесор и прави друго повикване за изтегляне, използвайки пикселен шейдър, който е конфигуриран да използва „четката“, конфигурирана в XAML.

Помните ли, че отбелязах непълнотата на елипсата? Наистина е. WPF генерира това, което Direct3D програмистите познават като "списък с редове". Графичният процесор разбира линии, както и триъгълници. WPF запълва буфера на върховете с тези редове и познайте какво? Добре, прави ли друго изтегляне? Наборът от линии изглежда така:

Сега WPF приключи с рисуването на елипсата, нали? Не! Забравихте за контура! Контурът също е набор от линии. Той също така се изпраща до буфера на върховете и се прави друго повикване за теглене. Контурът изглежда така

В този момент сме начертали една елипса, така че нашата рамка изглежда така:

Цялата процедура трябва да се повтори за всяка елипса в сцената. В нашия случай два пъти.
Не разбрах. Защо това е лошо за производителността?
Първото нещо, което може да забележите, е рендиранетоЗа една елипса имахме нужда от три извиквания за чертане и два достъпа до буфера на върха. За да обясня неефективността на този подход, ще трябва да говоря малко за работата на GPU. Като начало, съвременните графични процесори са МНОГО БЪРЗИ и асинхронни с графичния процесор. Но някои операции включват скъпи преходи от потребителски режим към режим на ядрото. Когато буферът на върховете е пълен, той трябва да бъде заключен. Ако буферът в момента се използва от GPU, той принуждава GPU да се синхронизира с CPU и драстично влошава производителността. Вертексният буфер се създава с D3DUSAGE_WRITEONLY D3DUSAGE_DYNAMIC, но когато се заключи (което не е необичайно), D3DLOCK_DISCARD не се използва. Това може да причини загуба на скорост (синхронизация на GPU и CPU) в GPU, ако буферът вече се използва от GPU. В случай на голям брой повиквания за теглене, имаме голяма вероятност да получим много преходи към режим на ядрото и голямо натоварване на драйверите. За да подобрим производителността, трябва да изпратим възможно най-много работа към графичния процесор, в противен случай вашият процесор ще бъде зает, а графичният процесор ще бъде неактивен. Имайте предвид, че в този пример говорихме само за един кадър. Типичният интерфейс на WPF се опитва да рендира с 60 кадъра в секунда! Ако някога сте се опитвали да разберете защо нишката ви за рендиране заема толкова много CPU, вероятно сте открили, че по-голямата част от натоварването идва от драйвера на GPU.
Какво ще кажете за кешираната композиция? Подобрява производителността!

Но WPF има и тъмни страни в този случай. За всеки BitmapCache той прави отделно извикване за теглене. Няма да лъжа, понякога наистина трябва да направите изтегляне, за да изобразите един обект(визуално). Всичко се случва. Но нека си представим сценарий, при който имаме 300 анимирани елипси BitmapCached. Една усъвършенствана система ще осъзнае, че трябва да изобрази 300 текстури и всички те са z-подредени една след друга. След това ще събере техните пакети с максимален размер, доколкото си спомням, DX9 може да приеме до 16 входящи елемента (входове за семплер) наведнъж. В този случай ще получим 16 повиквания за теглене вместо 300, което значително ще намали натоварването на процесора. От гледна точка на 60 кадъра в секунда ще намалим натоварването от 18 000 извиквания за изтегляне в секунда до 1125. В Direct 3D 10 броят на входящите елементи е много по-висок.
Добре, прочетох до тук. Кажете ми как WPF използва пикселни шейдъри!
WPF има API за разширяем пикселен шейдър и някои вградени ефекти. Това позволява на разработчиците да добавят наистина уникални ефекти към своя потребителски интерфейс. Когато опитвате шейдър върху съществуваща текстура в Direct 3D, обичайно е да използвате междинна цел за рендиране... и в крайна сметка не можете да семплирате от текстурата, върху която пишете! WPF прави и това, но за съжаление създава изцяло нова текстура ВСЕКИ КАДЪР и я унищожава, когато свърши. Създаването и унищожаването на GPU ресурси е едно от най-бавните неща, които можете да правите на базата на кадър. Обикновено не правя това дори при разпределение на системна памет с подобно количество. Чрез повторното използване на тези междинни повърхности може да се постигне много значително увеличение на производителността. Ако някога сте се чудили защо вашите хардуерно ускорени шейдъри причиняват значително натоварване на процесора, сега знаете отговора.
Но може бинаистина ли е необходимо да се рендират векторни графики на GPU?
Microsoft положи много усилия за коригиране на тези проблеми, за съжаление това не беше направено в WPF, а в Direct 2D. Погледнете тази група от 9 елипси, изобразени от Direct2D:

Спомняте ли си колко извиквания за чертане бяха необходими на WPF, за да изобразиединичнаелипса с контур? Какво ще кажете за заключванията на върховите буфери? Direct2D прави това с ЕДНО повикване за теглене. Теселацията изглежда така

Direct 2D се опитва да рисува възможно най-много наведнъж, като увеличава максимално използването на GPU и минимизира използването на CPU. Прочетете Insights: Direct2D Rendering в края на тази страница, където Марк Лорънс обяснява много от вътрешната работа на Direct 2D. Може да забележите, че въпреки скоростта на Direct 2D, има още повече области, в които ще бъде подобрен във втората версия. Възможно е версия 2 на Direct 2D да използва DX11 хардуерно теселационно ускорение.
Ами Silverlight?
Бих могъл да се заема със Silverlight, но това би било прекалено. Производителността на изобразяване в Silverlight също е лоша, но по различни причини. Ползва процесора за рендиране (дори за шейдърите доколкото си спомням са частично написани на асемблер), но процесора е поне 10-30 пъти по-бавен от графичния процесор. Това ви оставя с много по-малко процесорна мощност за изобразяване на потребителския интерфейс и още по-малко за логиката на вашето приложение. Неговото хардуерно ускорение е много недоразвито и почти точно имитира кешираната компилация на WPF и се държи по подобен начин, като прави изтегляне за всеки обект с BitmapCache (BitmapCached визуален).
И така, какво ще правим сега?
Това е много често срещан въпрос, който получавам от клиенти с проблеми с WPF.и Silverlight. За съжаление нямам категоричен отговор. Тези, които могат да направят своя собствена рамка, съобразена с техните специфични нужди. Останалите трябва да се примирят, тъй като няма алтернативи на WPF и SL в техните ниши. Ако моите клиенти просто разработват бизнес приложения, тогава те нямат много проблеми със скоростта и просто се наслаждават на производителността на програмистите. Истинските проблеми са тези, които искат да създадат наистина интересни интерфейси (т.е. потребителски приложения или павилионни приложения).