Ког­да уве­личи­вать количес­тво тран­зисто­ров на ядре мик­ропро­цес­сора ста­ло физичес­ки невоз­можно, про­изво­дите­ли начали помещать на один крис­талл нес­коль­ко ядер. Вмес­те с тем появи­лись фрей­мвор­ки, поз­воля­ющие рас­парал­леливать исполне­ние кода: Threading Building Blocks и Cilk от Intel, Concurrency Runtime (вклю­чает Parallel Patterns Library и Asynchronous Agents Library) и Task Parallel Library (пер­вый для натив­ного кода, вто­рой для управля­емо­го) от Microsoft и дру­гие. И это было толь­ко начало. Про­изво­дите­ли виде­оадап­теров — гра­фичес­ких про­цес­соров тоже не сто­яли на мес­те, они добав­ляли ядра не еди­ница­ми, а десят­ками и сот­нями. И прог­раммис­там это пон­равилось!

По­нача­лу гра­фичес­кие про­цес­соры были при­год­ны для весь­ма узко­го кру­га задач (уга­дай, каких), но выг­лядели очень соб­лазни­тель­но, и раз­работ­чики прог­рам­мно­го обес­печения решили вос­поль­зовать­ся их мощью, что­бы передать на гра­фичес­кие уско­рите­ли часть вычис­лений. Пос­коль­ку ГП невоз­можно исполь­зовать таким же обра­зом, как ЦП, понадо­бились новые инс­тру­мен­ты, которые не зас­тавили себя ждать. Так появи­лись CUDA, OpenCL и DirectCompute. Эта новая вол­на получи­ла имя GPGPU (General-purpose graphics processing units), обоз­нача­ющее тех­нику исполь­зования гра­фичес­кого про­цес­сора для вычис­лений обще­го наз­начения. Таким обра­зом, для решения весь­ма общих задач ста­ли исполь­зовать­ся нес­коль­ко совер­шенно раз­ных мик­ропро­цес­соров, что породи­ло наз­вание «гетеро­ген­ный парал­лелизм». Собс­твен­но, это и есть тема нашего сегод­няшне­го раз­говора.

На рын­ке уже появи­лось мно­го раз­ных инс­тру­мен­тов для вычис­лений с помощью гра­фичес­кого адап­тера. Давай пос­мотрим, какая тех­нология луч­ше все­го подой­дет для наших целей. Сра­зу же опре­делим, каким тре­бова­ниям она дол­жна отве­чать: быть мак­сималь­но плат­формо­неза­виси­мой (в том чис­ле прог­рам­мно и аппа­рат­но), лег­ко соп­ряга­емой с име­ющим­ся кодом и исполь­зуемы­ми инс­тру­мен­тами прог­рамми­рова­ния.

 

CUDA

Итак, CUDA — запатен­тован­ная тех­нология GPGPU от NVIDIA, и в этом сов­сем нет ничего пло­хого. Для написа­ния Cuda-скрип­тов исполь­зует­ся язык, близ­кий к C, но со сво­ими огра­ниче­ниями. В целом — зас­лужива­ющая вни­мания и доволь­но широко исполь­зуем­ся тех­нология. Меж­ду тем зависи­мость от кон­крет­ного вен­дора пор­тит всю кар­тину.

 

FireStream

FireStream — то же самое, что CUDA, толь­ко от AMD, поэто­му не будем задер­живать паци­ента.

 

OpenCL

OpenCL — откры­тый язык вычис­лений, сво­бод­ная, непатен­тован­ная тех­нология, весь­ма инте­рес­ное зре­лище, одна­ко в сво­ей осно­ве это весь­ма отличный от C язык (хотя раз­работ­чики говорят об обратном). В таком слу­чае прог­раммис­ту при­дет­ся изу­чать поч­ти пол­ностью новый язык с нес­тандар­тны­ми фун­кци­ями. Это навева­ет грусть. К тому же, так как не сущес­тву­ет стан­дарта на дво­ичный код, ком­пилятор от любого вен­дора име­ет пол­ное пра­во генери­ровать несов­мести­мый код, из чего сле­дует, что на каж­дой плат­форме шей­деры при­дет­ся переком­пилиро­вать, для чего необ­ходимы их исходные коды. Изна­чаль­но OpenCL был раз­работан в Apple, ну а сей­час им заведу­ет Khronos Group — так же, как OpenGL.

 

DirectCompute

DirectCompute — новый модуль DirectX 11, поз­воля­ющий осу­щест­влять опе­рации GPGPU. До вве­дения DirectCompute в DirectX при­сутс­тво­вал язык для реали­зации вычис­лений на гра­фичес­ком про­цес­соре, но они были завяза­ны исклю­читель­но на гра­фику. Час­ти при­ложе­ния, исполь­зующие DirectCompute, тоже прог­рамми­руют­ся на HLSL, толь­ко теперь этот код может слу­жить более общим целям.

Ло­гич­но пред­положить, что DirectCompute — детище Microsoft, но раз­вити­ем его занима­ются NVIDIA и AMD. В отли­чие от OpenCL, DirectCompute име­ет стан­дарт и ком­пилиру­ется в аппа­рат­но незави­симый байт‑код, что поз­воля­ет ему без переком­пиляции выпол­нять­ся на раз­ном обо­рудо­вании. В опе­раци­онных сис­темах, отличных от Windows и, соот­ветс­твен­но, не име­ющих под­дер­жки DirectX, для выпол­нения DirectCompute-кода исполь­зует­ся OpenCL. Как я говорил выше, код для DirectCompute пишет­ся на C-подоб­ном язы­ке HLSL, име­ющем свои осо­бен­ности (нес­тандар­тные типы дан­ных, фун­кции и про­чее).

 

AMP

Рис. 1. Ноутбук, на котором все это тестировалось
Рис. 1. Ноут­бук, на котором все это тес­тирова­лось

Ка­залось бы, ни одна тех­нология не обе­щает быть удоб­ной и дос­таточ­но про­дук­тивной. Одна­ко Microsoft при­гото­вила еще одну фичу — надс­трой­ку для язы­ка C++ — AMP (Accelerated Massive Parallelism — уско­рен­ный мас­сивный парал­лелизм). И вклю­чила ее под­дер­жку в ком­пилятор Visual C++, начиная с вер­сии, вошед­шей в Visual Studio 2012 (мно­гие инс­тру­мен­ты для работы с парал­лелиз­мом появи­лись толь­ко в сле­дующей вер­сии сту­дии — Visual Studio 2013).

В общем слу­чае есть два спо­соба рас­парал­лелива­ния прог­рамм: по дан­ным и по задачам. При работе с гра­фичес­ким про­цес­сором рас­парал­лелива­ние про­исхо­дит по пер­вому типу, так как ГП име­ет боль­шое количес­тво ядер, каж­дое из которых выпол­няет рас­четы незави­симо над сво­им набором дан­ных. Если на ЦП выпол­няемые задачи при­нято называть про­цес­сами (которые делят­ся на потоки и так далее), то задачи, выпол­няемые на ГП, называ­ют нитями (в Windows NT >= 5.1 тоже есть нити, но не будем при­дирать­ся к опре­деле­ниям). Таким обра­зом, у каж­дого ядра есть своя нить. AMP поз­воля­ет, исполь­зуя при­выч­ные средс­тва прог­рамми­рова­ния, рас­парал­леливать выпол­нение кода на гра­фичес­кие адап­теры, в слу­чае если их нес­коль­ко. AMP может работать со все­ми сов­ремен­ными ГП, под­держи­вающи­ми DirectX 11.

Тем не менее перед запус­ком кода на ГП его сов­мести­мость с AMP луч­ше заранее про­верить, чем мы зай­мем­ся в сле­дующем раз­деле.
В отли­чие от рас­смот­ренных выше тулз для GPGPU, где исполь­зованы диалек­ты C, в AMP исполь­зует­ся C++, со все­ми его дос­тоинс­тва­ми: типобе­зопас­ностью, исклю­чени­ями, перег­рузкой, шаб­лонами и про­чим.

От­сюда сле­дует, что раз­работ­ка гетеро­ген­ных при­ложе­ний ста­ла удоб­нее и про­дук­тивнее. Что осо­бен­но важ­но для чис­тоты язы­ка, AMP добав­ляет толь­ко два новых клю­чевых сло­ва к С++, в осталь­ном исполь­зуют­ся биб­лиотеч­ные средс­тва AMP (шаб­лонные фун­кции, типы дан­ных и так далее). Бла­года­ря это­му AMP на уров­не кода сов­местим с Intel TBB. Плюс к это­му Microsoft откры­ла спе­цифи­кацию на AMP всем жела­ющим. Таким обра­зом, сто­рон­ние раз­работ­чики могут не толь­ко рас­ширять AMP, но и перено­сить его на дру­гие прог­рам­мно‑аппа­рат­ные плат­формы, пос­коль­ку AMP раз­работан с заделом на будущее, ког­да код мож­но будет исполнять не толь­ко на ЦП и гра­фичес­ких уско­рите­лях.

 

AMP и поддерживаемые ускорители

На­пишем прог­рамму для получе­ния всех устрой­ств, под­держи­вающих AMP. Она будет выводить их спи­сок в кон­соль, так­же прог­рамма будет выводить зна­чения некото­рых важ­ных для AMP свой­ств уско­рите­лей.

Соз­дай кон­соль­ный Win32-про­ект; для под­клю­чения AMP не нуж­ны допол­нитель­ные тан­цы с нас­трой­кой ком­пилято­ра, дос­таточ­но толь­ко под­клю­чения заголо­воч­ного фай­ла — amp.h и прос­транс­тва имен — concurrency. Еще нуж­ны заголов­ки для под­клю­чения опе­раций вво­да‑вывода — iostream и iomanip. В фун­кции _tmain мы толь­ко уста­новим локаль и вызовем фун­кцию show_all_accelerators, которая выпол­нит необ­ходимую работу — выведет спи­сок аксе­лера­торов и их свой­ства. Эта фун­кция ничего не дол­жна воз­вра­щать, а внут­ри нее про­исхо­дят две опе­рации. В пер­вой мы от объ­екта клас­са accelerator с помощью ста­тич­ного метода get_all получа­ем век­тор, содер­жащий все дос­тупные уско­рите­ли. Вто­рое дей­ствие осу­щест­вля­ется с помощью алго­рит­ма for_each из прос­транс­тва имен concurrency. Этот алго­ритм выпол­няет лям­бду для каж­дого эле­мен­та век­тора:

std::for_each(accs.cbegin(), accs.cend(), [=, &n] (const accelerator& a)

Лям­бда, соот­ветс­твен­но, переда­ется треть­им парамет­ром, здесь показан толь­ко ее интро­дук­тор, в нем мы ука­зыва­ем ком­пилято­ру, что объ­ект аксе­лера­тора, выб­ранный из век­тора, переда­ем по зна­чению, а инкре­мен­тиру­емую перемен­ную n — по ссыл­ке. Внут­ри лям­бды мы прос­то выводим некото­рые свой­ства гра­фичес­кого адап­тера, как то: путь к устрой­ству (име­ется в виду шина), выделен­ная память, под­клю­чен ли монитор, находит­ся ли устрой­ство в отла­доч­ном режиме, эму­лиру­ется ли фун­кци­ональ­ность (с помощью ЦП), под­держи­вает­ся или нет двой­ная точ­ность, под­держи­вает­ся ли огра­ничен­ная двой­ная точ­ность (если да, в таком слу­чае устрой­ство поз­воля­ет осу­щест­влять не пол­ный набор вычис­лений, опре­делен­ные опе­рации не под­держи­вают­ся). В моем слу­чае (на ноут­буке с дву­мя виде­оадап­терами) вывод прог­раммы сле­дующий.

Рис. 2. Доступные ускорители
Рис. 2. Дос­тупные уско­рите­ли

Как вид­но, кро­ме двух физичес­ких уско­рите­лей, уста­нов­ленных у меня в ноуте, были обна­руже­ны еще три. Раз­берем­ся с ними. Software Adapter (REF) — прог­рам­мный адап­тер, эму­лиру­ющий ГП на ЦП, так­же называ­ется средс­твом прог­рам­мной отри­сов­ки. Он работа­ет гораз­до мед­леннее аппа­рат­ного ГП. При­сутс­тву­ет толь­ко в Windows 8. Исполь­зует­ся глав­ным обра­зом для отладки при­ложе­ний. CPU accelerator есть, как в Windows 8, так и в Windows 7. Тоже очень мед­ленный, пос­коль­ку работа­ет на ЦП, при­меня­ется для отладки. Microsoft Basic Renderer Driver — луч­ший выбор из эму­лиру­емых уско­рите­лей, так­же работа­ет на CPU, пос­тавля­ется в ком­плек­те с Visual Studio 2012 и выше. Он же известен как WARP (Windows Advanced Rasterization Platform). Для рен­дерин­га исполь­зует фун­кци­ональ­ность Direct3D. Повышен­ная ско­рость работы по срав­нению с дру­гими эму­лято­рами дос­тига­ется бла­года­ря при­мене­нию инс­трук­ций SIMD (SSE).

Кро­ме того, для раз­работ­ки и отладки C++ AMP при­ложе­ний рекомен­дует­ся исполь­зовать ОС Windows 8, и это утвер­жде­ние я готов аргу­мен­тировать. Как я говорил выше, во‑пер­вых, это под­дер­жка отладки на эму­лиру­емом уско­рите­ле, под­дер­жка вычис­лений с двой­ной точ­ностью, бла­года­ря спе­цифи­кации WDDM 1.2, уве­личен­ное количес­тво буферов (я про буферы DirectCompute), поз­воля­ющие запись (c под­дер­жкой DirectX 11.1). И самое глав­ное — из‑за того, что в Windows 8 во вре­мя копиро­вания дан­ных из уско­рите­ля в память ЦП гло­баль­ная бло­киров­ка ядра не зах­ватыва­ется (в отли­чие от положе­ния дел на Windows 7), опе­рация копиро­вания про­исхо­дит быс­трее, от чего воз­раста­ет общая про­изво­дитель­ность.

 

Элементы AMP

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

Мы уже видели объ­ект клас­са accelerator, пред­став­ляющий вычис­литель­ное устрой­ство. По умол­чанию он ини­циали­зиру­ется самым под­ходящим из име­ющих­ся аксе­лера­торов. Пос­ле получе­ния с помощью фун­кции get_all спис­ка всех при­сутс­тву­ющих уско­рите­лей ему методом set_default мож­но наз­начить дру­гой ГП, передав в парамет­ре путь к пос­ледне­му. Каж­дый уско­ритель (объ­ект клас­са accelerator) име­ет одно или нес­коль­ко изо­лиро­ван­ных логичес­ких пред­став­лений (находя­щих­ся в памяти виде­оадап­тера), в которых про­изво­дят вычис­ления нити, отно­сящи­еся к дан­ному кон­крет­ному ГП.

Объ­ект клас­са accelerator_view пред­став­ляет собой сво­его рода ссыл­ку на уско­ритель. Она поз­воля­ет более широко работать с объ­ектом: нап­ример, у тебя будет воз­можность обра­баты­вать исклю­чения TDR — обна­руже­ние тайм‑аута и вос­ста­нов­ление (такое исклю­чение про­исхо­дит, к при­меру, если ГП выпол­няет вычис­ления доль­ше двух секунд, при этом в Windows 7, в отли­чие от Windows 8, исклю­чения TDR нель­зя отклю­чить). Если это исклю­чение не обра­ботать и не передать вычис­ления на дру­гой accelerator_view, тог­да вос­ста­новить работу мож­но толь­ко переза­пус­ком при­ложе­ния.

Шаб­лонный тип array, как и сле­дует из наз­вания, пред­став­ляет набор дан­ных, пред­назна­чен­ных для вычис­лений на ГП. Эта кол­лекция соз­дает­ся в пред­став­лении ГП. Что­бы соз­дать кол­лекцию дан­ного типа, надо передать конс­трук­тору два парамет­ра: тип дан­ных и количес­тво объ­ектов дан­ного типа. Мож­но соз­дать мас­сив раз­ных раз­мернос­тей (до 128), зада­ется в конс­трук­торе или путем изме­нения его шаб­лонно­го типа extent <>; име­ются перег­ружен­ные конс­трук­торы; запол­нить мас­сив зна­чени­ями мож­но как на эта­пе его соз­дания (в конс­трук­торе), так и пос­ле (с помощью метода copy). Для опре­деле­ния позиции эле­мен­та в мас­сиве сущес­тву­ет спе­циаль­ный шаб­лонный тип index <>.

Тип array_view отно­сит­ся к типу array так же, как accelerator_view к accelerator, дру­гими сло­вами — пред­став­ляет собой ссыл­ку. Она может быть кста­ти, ког­да не нуж­но копиро­вать дан­ные из памяти ЦП в память ГП и обратно. Нап­ример, кол­лекция array всег­да находит­ся в памяти ГП, то есть в момент ее ини­циали­зации дан­ные копиру­ются из ЦП в ГП. С дру­гой сто­роны, если объ­явить объ­ект array_view на осно­ве век­тора из области ЦП, дан­ные век­тора не будут ско­пиро­ваны до момен­та непос­редс­твен­ной работы с ГП, а эта работа выпол­няет­ся внут­ри алго­рит­ма parallel_for_each.

Та­ким обра­зом, это единс­твен­ная точ­ка при­ложе­ния, где код рас­парал­лелива­ется для выпол­нения на аксе­лера­торе. Код выпол­няет­ся на том ГП, мас­сив которо­го передан алго­рит­му. В пер­вом парамет­ре parallel_for_each получа­ет объ­ект extent (или раз­мерность) мас­сива объ­ектов, для которых алго­ритм выпол­няет фун­кцию, передан­ную (вто­рым парамет­ром) пос­редс­твом фун­кто­ра, или лям­бды. В соот­ветс­твии с пер­вым парамет­ром будет запуще­но такое количес­тво потоков для выпол­нения. Сущес­тву­ет воз­можность внут­ри фун­кции или лям­бды (aka ядер­ной фун­кции) выз­вать дру­гую фун­кцию, но она дол­жна быть помече­на клю­чевым сло­вом restrict(amp). Если алго­ритм parallel_for_each при­нима­ет для выпол­нения фун­ктор (или лям­бда‑выраже­ние), то фун­кция или лям­бда, на которую он ука­зыва­ет, тоже дол­жна быть помече­на дан­ным клю­чевым сло­вом.

Име­ются еще некото­рые огра­ниче­ния на ядер­ную фун­кцию, нап­ример она может зах­ватывать (из внеш­него кода) толь­ко парамет­ры, переда­ваемые по ссыл­ке.

В ито­ге самое мед­ленное мес­то в любом при­ложе­нии, исполь­зующем вычис­ления на GPU, — это копиро­вание дан­ных из памяти ЦП в память ГП и обратно. Поэто­му необ­ходимо это учи­тывать, и, если вычис­лений нем­ного, то, ско­рее все­го, будет быс­трее их выпол­нить на CPU.

 

Применение AMP

Как ты заметил, AMP нераз­рывно свя­зан с DirectX, одна­ко это сов­сем не зна­чит, что с помощью AMP мож­но выпол­нять толь­ко гра­фичес­кие вычис­ления. Тем не менее гра­фика — это наибо­лее ресур­соем­кие вычис­ления, тре­бующие высокой ско­рос­ти работы, поэто­му наибо­лее инте­рес­ные и показа­тель­ные при­меры отно­сят­ся как раз к ней.

Итак, уста­новим DirectX SDK, выпуск от июня 2010 года (вер­сия, вклю­чающая 11-ю вер­сию интерфей­сов). Рас­смот­рим при­мер для работы с гра­фикой: вра­щение тре­уголь­ника, пос­тро­енно­го средс­тва­ми Direct3D 11. Открой про­ект DXInterOp. Если пос­тро­ить и запус­тить при­ложе­ние, то мы уви­дим сле­дующее изоб­ражение, толь­ко в динами­ке.

Рис. 3. Вычисление координат вершин треугольника происходит на видеоадаптере
Рис. 3. Вычис­ление коор­динат вер­шин тре­уголь­ника про­исхо­дит на виде­оадап­тере

Файл DXInterOpsPsVs.hlsl содер­жит вер­шинный и пик­сель­ный шей­деры, в фай­ле DXInterOp.h, кро­ме мак­росов безопас­ного уда­ления объ­ектов, объ­явле­на струк­тура дву­мер­ной вер­шины (Vertex2D), исполь­зуемая на про­тяже­нии всей прог­раммы. В фай­ле DXInterOp.cpp находит­ся основной код при­ложе­ния: соз­дание окна, ини­циали­зация гра­фичес­кой под­систе­мы: соз­дание, раз­рушение устрой­ства Direct3D, заг­рузка и соз­дание объ­ектов шей­деров, пос­тро­ение тре­уголь­ника, залив­ка и перери­сов­ка окна и так далее. Весь этот код исполь­зует фун­кци­ональ­ность Direct3D и потому не явля­ется темой нашего сегод­няшне­го раз­говора.

В фай­ле ComputeEngine.h находит­ся инте­ресу­ющая нас часть при­ложе­ния. Класс AMP_compute_engine отве­чает за пре­обра­зова­ние коор­динат вер­шин. В его конс­трук­торе соз­дает­ся ссыл­ка на объ­ект accelerator, который пред­став­ляет­ся устрой­ством Direct3D. Затем этот класс ини­циали­зиру­ет объ­ект m_data, который пред­став­лен уни­каль­ным ука­зате­лем на одно­мер­ный мас­сив вер­шин (объ­явленный ранее как Vertex2D). Рабочей лошад­кой клас­са выс­тупа­ет фун­кция run, в которой в алго­рит­ме parallel_for_each внут­ри лям­бда‑выраже­ния вычис­ляет­ся новая позиция коор­динат для поворо­та тре­уголь­ника:

parallel_for_each(m_data->extent, [=, &data_ref] (index<1> idx) restrict(amp){
DirectX::XMFLOAT2 pos = data_ref[idx].Pos;
data_ref[idx].Pos.y = pos.y * cos(THETA) - pos.x * sin(THETA);
data_ref[idx].Pos.x = pos.y * sin(THETA) + pos.x * cos(THETA);
});

Об­рати вни­мание на интро­дук­тор лям­бды: ука­зыва­ется, что data_ref типа array<Vertex2D, 1> переда­ется по ссыл­ке, а парамет­ром переда­ет объ­ект idx типа index, этот индекс явля­ется номером выпол­няемой в дан­ный момент нити. Собс­твен­но фун­кция run вызыва­ется пря­мо перед визу­али­заци­ей, поэто­му работа дол­жна выпол­нять­ся очень быс­тро.

Во мно­гом это надуман­ный при­мер, поворот объ­екта впол­не мож­но реали­зовать в том же потоке, где выпол­няют­ся дей­ствия Direct3D (ини­циали­зация, рен­деринг и про­чие). Одна­ко в этом при­мере отчетли­во вид­но раз­деление обя­зан­ностей меж­ду час­тями при­ложе­ния, и вычис­ление новых коор­динат для вер­шин про­исхо­дит очень быс­тро даже на соф­твер­ном уско­рите­ле, запущен­ном в отла­доч­ном режиме.

 

Блочные алгоритмы

Ког­да вычис­ления про­исхо­дят на GPU, то, в отли­чие от CPU, в них не исполь­зует­ся пре­иму­щес­тво ядер­ного кеша, пос­коль­ку ГП очень ред­ко исполь­зует дан­ные пов­торно. С дру­гой сто­роны, как и ЦП, ГП очень мед­ленно извле­кает дан­ные из гло­баль­ной памяти. Осо­бен­ность ГП зак­люча­ется в том, что чем бли­же находят­ся целевые бло­ки, тем быс­трее про­исхо­дит обра­щение к ним. Все‑таки обра­щение к дан­ным в кеш‑памяти про­исхо­дит в разы быс­трее. И мож­но нас­тро­ить алго­ритм таким обра­зом, что­бы он чаще обра­щал­ся к кешу, то есть сох­ранял и счи­тывал из него дан­ные. Для это­го нуж­но раз­бить дан­ные на бло­ки — шту­ка неп­ростая, но может при­нес­ти сущес­твен­ную поль­зу в повыше­нии ско­рос­ти выпол­нения алго­рит­ма.

В отли­чие от ЦП, где кеш в боль­шинс­тве слу­чаев авто­мати­чес­кий, в ГП кеш прог­рамми­руемый. Поэто­му прог­раммист дол­жен сам заботить­ся о нем. Мы можем опре­делить бло­ки, в которых будут выпол­нять­ся нити. При этом необ­ходимо выпол­нить два пре­дус­ловия: вмес­то прос­того индекса, как в неб­лочной прог­рамме, исполь­зовать блоч­ный индекс, плюс вос­поль­зовать­ся прог­рамми­руемым кешем аксе­лера­тора. За каж­дой нитью зак­репле­на область памяти в пос­леднем, и, что­бы раз­местить там перемен­ную, надо перед ее объ­явле­нием пос­тавить клю­чевое сло­во tile_static, дру­гими сло­вами — ука­зать на исполь­зование блоч­но‑ста­тичес­кой памяти. Перемен­ные, помечен­ные этим клю­чом, могут исполь­зовать­ся толь­ко внут­ри ядер­ной фун­кции. Пос­коль­ку блоч­но‑ста­тичес­кая память очень и очень неболь­шая, в ней обыч­но сох­раня­ют неболь­шие час­ти мас­сивов (кол­лекции array) из гло­баль­ной виде­опа­мяти:

tile_static int num[32][32];

Ал­горитм parallel_for_each име­ет перег­ружен­ную вер­сию, которая в качес­тве пер­вого парамет­ра при­нима­ет объ­ект клас­са tiled_extent — extent, раз­делен­ный на бло­ки в дву­мер­ном или трех­мерном прос­транс­тве. Вот при­мер:

parallel_for_each(extent<2>(size, size), [=, &input, &output] (index<2> idx) restrict(amp)

В этом при­мере име­ем мас­сив size*size в дву­мер­ном прос­транс­тве. Ког­да алго­рит­му parallel_for_each пер­вым парамет­ром переда­ется tiled_extent, то лям­бде переда­ется объ­ект tiled_index в том же прос­транс­тве, что tiled_extent:

parallel_for_each(extent<1>(number_of_threads).tile<_tile_size>(), [=](tiled_index<_tile_size> tidx) restrict(amp)

Внут­ри лям­бды из объ­екта tiled_index мож­но получить дос­туп как к гло­баль­ному, так и к локаль­ному индексу с помощью свой­ств global и local:

const int tid = tidx.local[0];
const int globid = tidx.global[0];

Две при­веден­ные выше модифи­кации — это лишь самые явные изме­нения, которые надо внес­ти в пер­вую оче­редь при перехо­де с неб­лочной на блоч­ную вер­сию кода. Глав­ная же задача прог­раммис­та при передел­ке кода — пере­осмыслить решение и модифи­циро­вать ядер­ную фун­кцию и вызыва­емый из нее код.

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

Рас­смот­рим такой слу­чай. Перед обра­бот­кой дан­ных внут­ри лям­бды они копиру­ются из кол­лекции гло­баль­ной в кол­лекцию в блоч­но‑ста­тичес­кой памяти. Пос­ле это­го вызыва­ется алго­ритм для обра­бот­ки кол­лекции в блоч­но‑ста­тичес­кой памяти. Но если, пред­положим, запол­нение мас­сива про­исхо­дило в соот­ветс­твии с индексом нити, то перед обра­бот­кой он может быть запол­нен не пол­ностью, так как нити выпол­няют­ся незави­симо и, дой­дя до вызова алго­рит­ма, ни одна нить не в сос­тоянии узнать, выпол­нилась ли каж­дая нить, то есть запол­нен ли мас­сив пол­ностью. В таком слу­чае перед вызовом алго­рит­ма надо вста­вить вызов метода wait объ­екта клас­са tile_barrier, который невоз­можно соз­дать незави­симо, но мож­но получить из объ­екта клас­са tile_index, передан­ного в лям­бду:

tile_static int num[32][32];
num[tidx.local[0]][tidx.local[1]] = arr[tidx.global];
tidx.barrier.wait(); // Получаем tile_barrier
if (tidx.local == index<2>(0,0)) {
num[0][0] = t[0][0] + num[0][1] + num[1][0] + num[1][1];
}
 

Заключение

AMP может быть исполь­зован не толь­ко из C++, но и из управля­емо­го кода, нап­ример на C#. Вдо­бавок при­ложе­ния для Windows Store тоже в пол­ной мере исполь­зуют C++ AMP — гетеро­ген­ный парал­лелизм на гра­фичес­ких уско­рите­лях, которые в нас­тоящее вре­мя есть не толь­ко в ПК, но и в план­шетах и смар­тфо­нах.

К сожале­нию, в статье нам уда­лось пос­мотреть толь­ко на вер­шину айсбер­га Microsoft AMP, огромная часть тех­нологии оста­лась нерас­смот­ренной. Я толь­ко обра­тил на нее твое вни­мание, даль­нейшее пос­тижение и изу­чение AMP передаю в твои руки. В зак­лючение хочет­ся отме­тить: в Visual Studio есть не толь­ко средс­тва для соз­дания рас­парал­лелен­ного кода, но и средс­тва для его отладки и визу­али­зации выпол­нения парал­лель­ных вычис­лений, что сегод­ня мы не успе­ли обсу­дить.

Вы­чис­ления на аксе­лера­торах рань­ше исполь­зовались ред­ко из‑за слож­ности, одна­ко AMP дела­ет гетеро­ген­ный парал­лелизм дос­тупным для широчай­ших масс прог­раммис­тов.

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии