Приемы и технологии программирования многоядерных процессоров
Модель ускорения вычислений (computational acceleration)
Эта модель является развитием предыдущей модели, более гибкой по сравнению с моделью выгрузки функций.
Выделяются одно или несколько управляющих ядер, остальные ядра выполняют наиболее вычислительно емкие участки кода программы. Необходимо распараллеливать программу и данные, организовывать эффективный обмен кодом и данными между ядрами.
Задача с уникальным набором данных и функций достаточно тяжеловесна для вызова и обслуживания. Многозадачный логический параллелизм предполагает небольшое количество задач (обычно в пределах одного десятка). Между тем существуют области, в которых необходим "мелкозернистый" логический параллелизм — разбиение задачи на множество небольших независимо исполняемых частей. В серверах баз данных, на новостных порталах, в мобильных сервисах, которые обслуживают распределенные системы, предназначенные для нескольких сотен тысяч абонентов, требуется механизм независимого обслуживания тысяч одновременно поступающих запросов. В таких системах надо не только не пропустить поступивший запрос, но и обслужить его, возможно, организовав интерактивный диалог, контроль над достоверностью и перезапрос информации в случае ошибки.
"Мелкозернистый" логический параллелизм эффективен в задачах управления сложным промышленным оборудованием. Необходимость отражения параллелизма процессов, осуществляемых на объекте управления, требует создания по меньшей мере сотни параллельно исполняемых процессов. Например, эквивалентное монолитное решение в виде одного процесса, предназначенное для управления выращиванием монокристаллического кремния [90], приводит к рассмотрению 1030 различных ситуаций. Сжатие информации достигается при логическом параллелизме за счет упрощенного описания комбинаций независимых случаев: описание алгоритма как набора независимых частей — это описание суммы ситуаций, а монолитное описание — это описание произведения ситуаций. Невообразимое число ситуаций (1030) можно получить лишь из сотни параллельно исполняемых компонентов с двумя состояниями, в то время как независимое описание алгоритма подразумевает рассмотрение всего 200 уникальных случаев.
Основными целями логического параллелизма являются сокращение времени разработки, упрощение создания, эксплуатации и, в общем случае, сопровождения программных систем. В том и другом случаях предполагается выделение участков алгоритма, допускающих независимое или слабо зависимое исполнение.
Потоковые модели (streaming models)
Преимущества "мелкозернистого" логического параллелизма заставляют искать пути снижения накладных расходов, присущих многозадачности. Известные решения для операционных систем — легковесные процессы (light-weight process). Так, в Sun OS 5.x имеется облегченный вариант задач — потоки (thread), состояние которых полностью характеризуется значениями указателей на код и стек. Переключение процессора на поток минимизировано вплоть до операций сохранения/восстановления этих указателей. Планировщик попрежнему присутствует и активизируется по прерываниям от таймера [90].
Переключение потоков по времени не всегда удобно и приводит к непроизводительным простоям процессора. Если в потоке для продолжения вычислений требуется внешнее событие (отклик оборудования, реакция абонента на запрос, подтверждение успешной передачи сообщения), то поток "честно" тратит отведенное ему время на ожидание:
Step1(); /* действие */ while (!EventOnStep1()); /* ожидание реакции на действие */ Step2(); /* следующее действие */
Если для реакции на действие требуется значительное время (например, событие — это реакция пользователя на запрос системы), то более приемлемая стратегия — передача управления после опроса. Этот подход используется при организации многопоточности методом циклического опроса (round-robin), который иногда обозначают термином "корпоративная многозадачность" (довольно часто такая модель многопоточности реализуется в некоторых системах реального времени, в некоторых версиях языка Форт). При циклическом опросе потоки могут быть представлены просто функциями, которые опрашиваются в бесконечном цикле:
Main(){ for (;;) { thread_1(); thread_2(); ... thread_N(); } }
Необходимость ожидания внешней реакции естественным образом задает разбиение потоков на части, любая из которых является простейшей функцией. Если присвоить каждой части какое-либо число, то реализация потоков естественным образом будет выражаться в терминах языка Си. По историческим причинам простейшие функции называют "состояниями".
thread_i () { switch (State_i) { ... case STATE_1: Step1(); /* действие */ if (EventOnStep1()) State_i = STATE_2; /* смена состояния */ break; case STATE_2: Step2(); /* следующее действие */ ... } }
Таким образом, при многопоточности можно минимизировать или даже полностью исключить накладные расходы, присущие многозадачности. Простота организации делает многопоточность привлекательной для обеспечения "мелкозернистого" логического параллелизма с гранулярностью на уровне нескольких инструкций. Получаемый выигрыш компенсирует риск возможных ошибок (которые могут возникнуть, например, из-за работы с общей областью данных). Мало того, эти риски можно полностью исключить за счет применения соответствующих механизмов.
В языке Рефлекс (диалект языка Си, предназначенный для описания алгоритмов управления) безопасность и невозможность разрушения программы языковыми средствами обеспечены на концептуальном уровне. В этом языке синтаксис и семантика Си расширены за счет понятия процесса — параллельно исполняемого участка кода, допускающего запуск, остановку и проверку своего состояния [90, 91]. Автоматизированы рутинные операции, связанные с организацией параллелизма, и пользователь может полностью сосредоточиться на собственно алгоритме. Возможность обращения по произвольному адресу исключена. Переменные языка типизированы. При таком подходе конфликты памяти попросту невозможны.
Например, приложение, разработанное для выполнения на процессорах с технологией Hyper-Threading, сразу получит преимущества от использования на двухъядерных процессорах, т. к. в нем уже предусмотрены два потока. В идеале при проектировании приложений необходимо предусматривать возможность создания максимального числа потоков в зависимости от количества и возможностей процессоров. Если программа может использовать дополнительные потоки, которые предоставляет технология Hyper-Threading или многоядерность, ее производительность будет напрямую зависеть от количества процессоров.
Если при разработке программы предусмотрено создание максимального количества потоков, возможных для нее, то по мере добавления новых ядер программа будет работать более быстро и эффективно. Принцип независимости количества потоков от количества реально имеющихся ядер позволяет создавать приложения, рассчитанные на перспективу, ускорить окупаемость инвестиций, сократить затраты и продлить жизненный цикл ПО.
Разработчики ПО столкнутся с различными проблемами: с необходимостью создания масштабируемых приложений, освоения новых методов отладки и написания кода, допускающего поддержку. В целом им придется использовать больше абстракций, чтобы избежать явной привязки к потокам. Большинству поставщиков инструментальных средств разработки следует уделять особое внимание этому вопросу. Абстракции, такие как библиотеки, OpenMP и многопоточные компоненты Intel Threading Blocks, очень важны для практической реализации масштабируемости. Наконец, разработка приложений связана с такими проблемами, как вероятность возникновения взаимоблокировки и ошибки синхронизации (известных как общие ошибки параллельного программирования). Intel
Thread Checker помогает бороться с такими опасностями. В качестве положительного факта необходимо заметить, что инструментальные средства постоянно развиваются, и скоро их поставщики найдут способы дальнейшей поддержки параллельного программирования. Разработчики инструментальных ср
едств для параллельного программирования и адаптации имеющихся приложений должны понимать проблемы, связанные с масштабируемостью, отладкой и поддержкой кода.
Модель мультипроцессора с разделяемой памятью (shared-memory multiprocessor model)
В мультипроцессорах с общей памятью (сильносвязанных мультипроцессорах) имеется память данных и команд, доступная всем ПЭ. С общей памятью ПЭ связываются с помощью общей шины или сети обмена. В противоположность этому варианту в слабосвязанных многопроцессорных системах (машинах с локальной памятью) вся память делится между процессорными элементами и каждый блок памяти доступен только связанному с ним процессору. Сеть обмена связывает процессорные элементы друг с другом.
Базовой моделью вычислений на MIMD-мультипроцессоре является совокупность независимых процессоров, эпизодически обращающихся к разделяемым данным. Существует большое количество вариантов этой модели. На одном конце спектра — модель распределенных вычислений, в которой программа делится на довольно большое число параллельных задач, состоящих из множества подпрограмм. На другом конце спектра — модель потоковых вычислений, в которых каждая операция в программе может рассматриваться как отдельный процесс. Такая операция ждет своих входных данных (операндов), которые должны быть переданы ей другими процессами. По их получении операция выполняется, и ее результат передается тем процессам, которые в нем нуждаются. В потоковых моделях вычислений с большим и средним уровнем гранулярности процессы содержат большое число операций и всегда выполняются в потоковой манере.
Модель ассиметричных потоков (asymmetric thread runtime model)
В то время как существующие массовые решения основаны на размещении в одном процессоре нескольких идентичных ядер, вполне очевидно, что в будущих разработках будут задействованы различные типы ядер на одном процессорном кристалле.
Данный подход обычно именуется асимметричной или неоднородной многоядерной архитектурой. Хотя эти термины имеют синонимичное значение, при описании программного обеспечения чаще используется термин "асимметричный" (что указывает на способ работы программного обеспечения с аппаратной архитектурой), а применительно к оборудованию чаще используется термин "неоднородный" (поскольку он более точно описывает собственно физическую архитектуру) [95].
Модель ассиметричных потоков является расширением широко распространенной модели многопоточности на случай вычислительных модулей с различной архитектурой инструкций.
К примеру, некоторые ядра не поддерживают аппаратную смену контекста, поэтому на них нельзя организовать вытесняющую многозадачность (preemptive multitasking). На таких ядрах возможно применять модель работы потока до завершения (run-to-completion model), а также кооперативную многозадачность (cooperative multitusking).
Возможным путем увеличения производительности в рамках микроархитектуры заданного размера является создание конфигураций из неоднородных ядер, обладающих различными функциональными характеристиками и быстродействием. Чтобы осознать эффективность такого решения, представим себе ситуацию, когда в одном процессоре содержится несколько крупных ядер с высокой последовательной производительностью и множество более мелких и менее производительных ядер.
В данном случае крупные ядра будут лучше приспособлены для работы с ресурсоемкими последовательными задачами, в то время как множество более мелких ядер обеспечит высокую производительность для задач с большим количеством параллельных потоков. Подобное сочетание позволит достичь более высокой средней концентрации ядер на площади кристалла (по сравнению с расположением однородных ядер), а возможность выделять те или иные комбинации ядер для обработки определенных нагрузок поможет добиться большей пространственной эффективности — и повышенной производительности.
При распределении нагрузок между аппаратными ресурсами уход от принципа "один размер для всех" дает несомненные преимущества, поскольку для одних нагрузок производительность ядер может иметь большее значение, чем для других. Например, требующие большого объема вычислений задачи уровня приложений лучше передать самому мощному процессорному ядру, в то время как другие задачи, связанные с вводом-выводом или доступом к памяти, могут без существенных потерь выполняться на менее производительном ядре.
В том случае, если менее ресурсоемкие задачи выполняются на менее производительных ядрах, общая производительность, как отношение задействованных ресурсов процессора, будет выше. Таким образом, процессор в целом будет демонстрировать большее быстродействие. Кроме того, выделение ресурсов с учетом требований тех или иных задач обеспечивает более эффективное использование потребляемой энергии в целом.
Программное обеспечение может учитывать только способность ядра выполнить определенный объем вычислений за большее или меньшее время. В данном примере в программном обеспечении не принимаются в расчет другие факторы, в частности, поддерживает ли то или иное ядро определенные функции.
Функциональная асимметрия между процессорными ядрами также может выражаться в различиях на уровне архитектуры набора команд (Instruction Set Architecture, ISA). В процессоре могут присутствовать ядра общего назначения с различиями в поддержке определенных наборов команд; например, в целях уменьшения сложности и размеров кристалла имеет смысл предусмотреть ядра с поддержкой набора команд SSE4 наряду с более простыми ядрами, не имеющими такой поддержки.
Другие виды функциональной асимметрии могут заключаться во всевозможных различиях логической архитектуры, с точки зрения регистров, типов данных, режимов адресации, архитектуры памяти, особенностей обработки исключений и прерываний, взаимодействия с внешними устройствами ввода-вывода. В противовес различиям в производительности подобные функциональные различия между ядрами должны быть соответствующим образом учтены в программном обеспечении, во избежание сбоев в процессе выполнения. Это может быть особенно актуально в тех случаях, когда функциональные особенности накладываются друг на друга, например, если некий набор ядер работает с общими типами данных и имеет общую архитектуру памяти, но при этом только часть ядер поддерживает определенный набор команд.
Во многих отношениях принципы программирования для функционально асимметричных многоядерных процессоров основываются на существующих наработках в области такого оборудования, как графические ускорители, программируемые логические матрицы (Field-Programmable Gate Arrays, FPGA), и специализированные интегральные схемы (Application Specific Integrated Circuits, ASIC), применяемые в качестве сопроцессоров.
Поскольку традиционные операционные системы не поддерживают асимметричные ресурсы процессора, распределение задач между такими ресурсами вносит элемент непредсказуемости, что может ограничивать масштабируемость. С другой стороны, поиск решений для снижения такой непредсказуемости представляет собой перспективную возможность для поставщиков программного обеспечения.
В целях оптимального распределения нагрузки между асимметричными ядрами следует принять во внимание ряд соображений. В частности, потоки должны быть сопоставлены ядрам, которые поддерживают необходимые инструкции и обладают другими требуемыми характеристиками. Необходимо предусмотреть эффективные средства обработки ошибок на тот случай, если такая поддержка отсутствует. При использовании асимметричных аппаратных ресурсов также можно ожидать дополнительных трудностей, касающихся сбалансированности потоков в многопоточном программном обеспечении, так как определенному потоку могут быть назначены ресурсы различных типов, в зависимости от предъявляемых требований и приоритета.
В целях преодоления этих и других трудностей специалистами Intel Systems Technology Lab был разработан AMPS — пакет средств асимметричной мультипроцессорной обработки задач, который улучшает производительность при выполнении задач на многоядерных процессорах, имеющих асимметрию на уровне производительности. Помимо повышения производительности AMPS также обеспечивает стабильное распределение потоков между соответствующими ресурсами по принципу приоритетности, позволяя добиться предсказуемых и воспроизводимых результатов. Следует отметить, что для работы с AMPS не требуется вносить изменения в приложения, а для операционной системы Linux потребуются лишь незначительные изменения, что создает опытно-экспериментальную базу для реализации упрощенной поддержки асимметричного оборудования.
AMPS распределяет нагрузку между ядрами в соответствии с их индивидуальными характеристиками, назначая задачи таким образом, чтобы обеспечить высокий общий коэффициент полезного использования ЦП. В этом обработчике используется приоритезация по принципу "более быстрое ядро — в первую очередь", что помогает задействовать самые производительные из доступных ядер. В архитектурах с неоднородным доступом к памяти (Non-Uniform Memory Access, NUMA) AMPS помогает преодолеть дополнительные затруднения, связанные, например, с миграцией потоков между неодинаковыми ядрами, соединенными с разными контроллерами памяти. По мере развития асимметричных многоядерных процессоров такие разработки, как AMPS, предоставят основу для создания программного обеспечения, поддерживающего эту технологию.
В рамках других подходов программную поддержку асимметричного оборудования предполагается реализовать на уровне приложения. Хотя такой подход повлечет за собой неизбежное усложнение стоящих перед программистом задач, независимость от операционной системы может стать существенным преимуществом, например, в условиях среды с закрытым исходным кодом. В частности, архитектура среды потоковой обработки множества команд (Multiple Instruction Stream Processing, MISP) позволяет программным путем представить процессорные ядра в качестве абстракций, которые могут управляться прикладным программным обеспечением. Программная архитектура и модель программирования EXOCHI расширяет возможности MISP, добавляя поддержку ядер, основанных на отличной от x86 архитектуре, а также предоставляя особую среду программирования C/C++ для реализации этих возможностей.
Краткие итоги
Основные моменты, требующие внимания при разработке параллельных программ:
- взаимные блокировки параллельных участков;
- несинхронный доступ, или гонки;
- возможность зависания параллельных участков;
- опасность использования сторонних процедур и библиотек;
- набор специализированных средств отладки физически параллельных программ;
- нелокальный характер ошибок;
- динамический характер ошибок и, как следствие, влияние средств отладки программ на корректность исполнения последних.
Параллельная обработка имеет две разновидности: конвейерность и собственно параллельность. Кроме конвейеризации и физического распараллеливания используются следующие методы:
- специализация (применение внутри процессоров блоков, оптимизированных под определенный вид вычислений, например математических сопроцессоров);
- кэширование;
- спекулятивные вычисления.
Основные модели программирования многоядерных процессоров — модель выгрузки функций, модель ускорения вычислений, потоковые модели, модель мультипроцессора с разделяемой памятью, модель ассиметричных потоков.
Контрольные вопросы
- Какие существуют основные приемы и технологии программирования многоядерных процессоров?
- В чем состоит суть модели выгрузки функций (function offload model)?
- Как устроена модель ускорения вычислений (computational acceleration).
- Укажите основные черты потоковой модели (streaming model) программирования.
- Дайте характеристику модель мультипроцессора с разделяемой памятью (shared-memory multiprocessor model).
- Чем отличается модель ассиметричных потоков (asymmetric thread runtime model) от потоковой модели программирования ?
Упражнения
- Оцените, какие из моделей программирования лучше подходят для процессоров из упомянутых выше.