Некоторые требования к проектированию программного обеспечения для многоядерных многопроцессорных архитектур

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

Гурудутт Кумар, разработчик программного обеспечения, IBM

guruduttГурудутт Кумар (Gurudutt Kumar) – разработчик программного обеспечения, сотрудник группы IBM Rational ClearCase — Multi-Version File System. Он работает в ИТ-отрасли более девяти лет и специализируется в таких областях, как операционные системы, файловые системы и программирование ядра.



19.03.2014

Введение

Компьютерные аппаратные средства быстро эволюционируют. Плотность транзисторов увеличивается, в то время как тактовые частоты постепенно стабилизируются. Производители процессоров стремятся наращивать мультипроцессорные возможности за счет увеличения количества ядер и аппаратных потоков в пересчете на один процессорный чип. Несколько примеров. Симметричная мультипроцессорная архитектура IBM POWER7® обеспечивает массовый параллелизм, поддерживая 4 потока на ядро, 8 ядер на процессорный чип и 32 процессорных сокета на сервер. В общей сложности это составляет 1024 одновременных аппаратных потока. Для сравнения — архитектура IBM POWER6® поддерживала лишь 2 потока на ядро, 2 ядра на чип и 32 сокета на сервер, что в общей сложности составляло 128 параллельных аппаратных потоков.

Сегодня при разработке программного обеспечения проектировщики обязаны учитывать особенности многопроцессорных многоядерных архитектур, на которых потенциально может быть развернуто это программное обеспечение. Это обусловлено следующими причинами.

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

В данной статье рассматриваются некоторые ключевые требования к проектированию программного обеспечения для многоядерных многопроцессорных сред.

Препятствия для масштабирования программного обеспечения на многоядерных многопроцессорных архитектурах с внутриядерной многопоточностью

Предполагается, что приложения будут лучше масштабироваться и исполняться в многоядерных многопроцессорных средах. Однако нерационально спроектированное приложение может плохо функционировать в такой среде и не оправдывать надежд на хорошую масштабируемость и на повышение производительности за счет использования доступных вычислительных ресурсов. Рассмотрим основные причины подобного ухудшения масштабируемости

  • Неэффективное распараллеливание. Монолитное приложение или иное программное обеспечение не сможет эффективно использовать доступные вычислительные ресурсы. Необходимо организовывать приложение в виде параллельных задач. Эта проблема регулярно отмечается в унаследованных приложениях и в программных продуктах, не поддерживающих многопоточность. Такие приложения не в состоянии масштабироваться и обеспечивать повышение пропускной способности на многоядерных многопроцессорных аппаратных средствах с внутриядерной многопоточностью. Слишком большое количество потоков может оказаться столь же негативным фактором, как и слишком малое их количество.
  • Узкие места последовательного исполнения.Приложения, у которых структуры данных совместно используются несколькими потоками или процессами, могут иметь узкие места последовательного исполнения. Чтобы поддержать целостность данных, доступ к этим совместно используемым структурам данных приходится сериализовать с помощью различных методов блокировки и сериализации (блокировка чтения, блокировка чтения-записи, блокировка записи, взаимоблокировка (spinlock), взаимное исключение (mutex) и т. д.). Неэффективно спроектированные блокировки могут породить узкие места последовательного исполнения вследствие высокой конкуренции между несколькими потоками или процессами, пытающимися установить блокировку. Это потенциально способно снизить производительность приложения или иного программного обеспечения. Производительность приложения может ухудшаться по мере увеличения количества ядер или процессоров.
  • Чрезмерная надежда на операционную систему или на среду исполнения.Не следует рассчитывать на то, что операционная система, среда исполнения или компилятор сделают все необходимое для масштабирования приложения или иного программного обеспечения. Хотя компиляторы и среды исполнения способны оказать определенное содействие при оптимизации, нельзя надеяться на то, что они решат все проблемы масштабируемости. Например, нельзя рассчитывать на то, что Java™-машина (JVM) выявит возможности для улучшения масштабируемости Java-приложения за счет его автоматического распараллеливания.
  • Узким местом может оказаться несбалансированность рабочих нагрузок .Неравномерное распределение рабочей нагрузки может снизить эффективность использования вычислительных ресурсов. Может возникнуть необходимость разделения больших задач на задачи меньшего размера, способные исполняться одновременно друг с другом. Кроме того, для улучшения производительности и масштабируемости, возможно, придется преобразовать последовательные алгоритмы в параллельные.
  • Узкие места ввода-вывода.Узкие места вследствие блокирования дискового ввода/вывода или больших сетевых задержек могут серьезно ухудшить масштабируемость приложения.
  • Неэффективное управление памятью.На многоядерных платформах "чистая" вычислительная мощность может обходиться недорого по причине большого количества процессорных блоков и вполне достаточного объема оперативной памяти (который к тому же непрерывно растет). Однако пропускная способность памяти остается постоянным узким местом, поскольку все процессорные ядра совместно используют общую шину. Неэффективное управление памятью может порождать трудновыявляемые проблемы с производительностью, такие как false sharing (ложное совместное использование).

Очевидно, что низкий коэффициент использования процессоров равносилен неоптимальному использованию ресурсов. Чтобы понять проблемы с производительностью, проектировщик должен оценить наличие у приложения слишком малого или слишком большого количества потоков, проблем блокировки или синхронизации, сетевых задержек или задержек ввода-вывода, "пробуксовки" памяти (memory thrashing) и других проблем, связанных с управлением памятью. Как правило, высокий коэффициент использования процессоров является положительным фактором, если ресурсы расходуются на потоки приложения, выполняющие осмысленную работу.


Обзор многоядерных многопроцессорных систем с внутриядерной многопоточностью

Прежде чем обсуждать проектировочные требования для многоядерных многопроцессорных сред с внутриядерной многопоточностью, бросим беглый взгляд на такую систему. Система на рис. 1 имеет два процессора, у каждого из которых есть два ядра, а в каждом из этих ядер есть два аппаратных потока. Каждое ядро имеет один кэш первого уровня (L1) и один кэш второго уровня (L2). Относительно кэша L2 возможны два варианта: 1) каждое ядро имеет собственный кэш L2; 2) ядра, находящиеся в одном и том же процессоре, совместно используют кэш L2. Аппаратные потоки в одном и том же ядре совместно используют кэш L1 и кэш L2.

Рисунок 1. Типичная многоядерная многопроцессорная система с внутриядерной многопоточностью
A block diagram depicting chip-multithread, multi-core, multi-processor system.

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

Следующие ключевые концепции помогут нам понять проблемы проектирования приложений для такой многоядерной многопроцессорной среды с внутриядерной многопоточностью.

Когерентность кэша

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

Рассмотрим следующий пример. Предположим, что в системе, показанной на рис. 1, поток 1 исполняется на процессоре 0, а поток 2 исполняется на процессоре 1. Если оба этих потока читают и записывают один и тот же элемент данных, то системе приходится выполнять дополнительные операции, чтобы гарантировать, что эти потоки будут видеть одно и тот же значение данных при каждой операции чтения и записи.

Когда поток 1 записывает элемент данных, который используется совместно с потоком 2, этот элемент данных обновляется в кэше его процессора и в системной памяти, но не сразу обновляется в кэше процессора потока 2, поскольку потоку 2 доступ к этому элементу данных может больше не требоваться. Если впоследствии поток 2 обращается к этому элементу данных, подсистема кэша на его процессоре сначала должна получить новое значение данных из системной памяти. Так, если поток 1 осуществит запись, то в следующий раз, когда поток 2 обратится к этим данным, ему придется ждать чтения из системной памяти. Такая последовательность возникает только в том случае, если изменение данных осуществляет один из вышеупомянутых потоков. Если каждый поток осуществляет серию записей, то производительность системы может серьезно ухудшиться вследствие совокупных затрат времени на ожидание, пока значения данных будут обновлены соответствующими значениями из системной памяти. Эта ситуация носит название «пинг-понг» (ping ponging); ее избежание является важным требованием при проектировании программного обеспечения для исполнения на многопроцессорных и многоядерных системах.

Отслеживание

Подсистема кэш-памяти отслеживает состояние каждой строки кэша. Эта подсистема использует метод под названием bus snooping (другое название —bus sniffing), отслеживая все транзакции, происходящие с участием системной шины, с целью обнаружения операций чтения или записи по адресу, который находится в ее кэше.

Когда подсистема кэш-памяти обнаруживает в системной шине операцию чтения из области памяти, загруженной в ее кэш, она изменяет состояние строки кэша на shared (используемая совместно). Если подсистема обнаруживает операцию записи по этому адресу, она изменяет состояние строки кэша на invalid (недействительная).

Поскольку подсистема кэш-памяти отслеживает системную шину, она распознает ситуацию, когда в ее кэше находится единственная копия определенных данных. Если данные обновляются собственным центральным процессором этой подсистемы кэш-памяти, она изменяет состояние соответствующей строки кэша с exclusive (единственная) на modified (измененная). Если подсистема кэш-памяти обнаружит попытку чтения по этому адресу со стороны другого процессора, она сможет воспрепятствовать этой попытке, обновить данные в системной памяти, а затем разрешить этому процессору продолжить доступ. Кроме того, она пометит состояние соответствующей строки кэша как shared.

За дополнительной информацией об этих концепциях обратитесь к статье Software Design Issues for multi-core multi-processor systems в разделе Ресурсы.


Влияние многоядерных многопроцессорных сред на проектировочные решения при создании программного обеспечения

При проектировании программного обеспечения, предназначенного для функционирования на многоядерной или многопроцессорной системе, основная задача состоит в распределении подлежащей выполнению работы между доступными процессорами. Наиболее распространенный способ распределения работы состоит в применении модели поточной обработки, при которой работа секционируется на отдельные блоки, способные параллельно исполняться на разных процессорах. Если эти потоки полностью независимы друг от друга, то при их проектировании можно не учитывать взаимодействие потоков. Например, две программы, исполняющиеся в системе как два отдельных процесса, каждому из которых предоставлено собственное ядро, не знают о существовании друг друга. Производительность этих программ не снижается, если они не конкурируют за какой-либо общий ресурс, такой как системная память или одно и тот же устройство ввода-вывода.

Последующее обсуждение будет посвящено взаимодействию ядер и процессоров с оперативной памятью и его влиянию на проектировочные решения при создании программного обеспечения.

Рассмотрим перечисленные ниже основные проектировочные требования.


Избегайте конкуренции за память

Разные ядра совместно используют общую область данных в памяти и в кэше, которые должны синхронизироваться между этими ядрами. Конкуренция за память (Memory contention) или конфликт в памяти— это ситуация, когда различные ядра одновременно обращаются к одной и той же области данных. Необходимость синхронизации данных между разными ядрами существенно снижает производительность вследствие перемещения данных по шине, потерь на блокировку и неудачных обращений в кэш (промахов кэша).

Если приложение имеет несколько потоков, а все эти потоки обновляют или изменяют один и тот же адрес памяти, то, как указывалось в предыдущем разделе, при обеспечении когерентности кэша может возникнуть ситуация «пинг-понга». Это приведет к снижению производительности.

За дополнительной информацией обратитесь к разделу Memory Contention в статье Memory issues on multi-core platforms, ссылка на которую приведена в разделе Ресурсы. Эта статья содержит простую программу, которая демонстрирует вредные последствия конкуренции за память. Этот пример демонстрирует, что и в том случае, когда всего лишь одна переменная совместно используется несколькими потоками, потеря производительности может оказаться весьма существенной, даже если для обновлений используются атомарные операции.

Методы исключения конкуренции за память

  • Избегайте совместного использования перезаписываемого состояния несколькими ядрами.
    • Чтобы минимизировать трафик по шине памяти, необходимо минимизировать взаимодействия между ядрами, сведя к минимуму совместно используемые местоположения/данные, даже если такие данные защищены не блокировкой, а какими-либо атомарными операциями аппаратного уровня (пример: InterlockedExchangeAdd64 на 32-разрядной платформе Microsoft® Windows®).
    • Один из способов ослабления конкуренции между потоками за память состоит в том, чтобы исключить возможность обновления области общей памяти несколькими потоками. К примеру, даже в том случае, если обновления глобального счетчика или "суммы нарастающим итогом" (при статистических расчетах) должны осуществляться несколькими потоками, отдельные потоки смогут поддерживать локальные "суммы нарастающим итогом", а обновление глобальной "суммы нарастающим итогом" будет производиться общим потоком только при возникновении реальной потребности. В результате конкуренция за область совместно используемой памяти значительно уменьшится.
    • Шаблоны проектирования, ослабляющие конкуренцию за право блокировки, также обычно уменьшают трафик памяти, поскольку именно совместно используемое перезаписываемое состояние требует блокировок и порождает конкуренцию.
  • Избегайте ложного совместного использования (false sharing), порождаемого кэшем ядра. Дополнительная информация приведена в следующем разделе.

Избегайте ложного совместного использования (false sharing)

Если два процессора (или более) записывают данные в разные части одной и той же строки кэша, то для эффективного признания недействительной или обновления каждой кэшируемой копии старой строки на других процессорах может потребоваться большой объем трафика через кэш и шину. Это явление носит название false sharing (ложное совместное использование совместное использование) или CPU cache line interference (помехи в строках кэша центрального процессора). В отличие от истинного совместного использования (true sharing), при котором два потока (или более) совместно используют те же самые данные (что обуславливает потребность в программных механизмах синхронизации, гарантирующих поочередный доступ), ложное совместное использование происходит в случае, когда два потока (или более) обращаются к несвязанным данным, которые находятся в одной и той же строке кэша.

Рассмотрим следующий фрагмент кода, чтобы лучше понять ложное совместное использование (Ссылка: False Sharing; материал опубликован на сайте Intel Developer Zone).

Листинг 1. Пример кода для иллюстрации ложного совместного использования
            double sumLocal[N_THREADS];
            . . . . .
            . . . . .
            void ThreadFunc(void *data)
            {
                  . . . . . . .
                  int id = p->threadId;
                  sumLocal[id] = 0.0;
                  . . . . . .
                  . . . . . .
                  for (i=0; i<N; i++)
                  sumLocal[id] += p[i];
                  . . . . . .
            }

В приведенном примере кода размер переменной sumLocal соответствует количеству потоков. Массив sumLocal потенциально способен вызвать ложное совместное использование, поскольку запись в этот массив осуществляют несколько потоков, если изменяемые ими элементы находятся в одной и той же строке кэша. На рис. 2 иллюстрируется ложное совместное использование между потоком 0 и потоком 1 при изменении двух следующих друг за другом элементов в массиве sumLocal. Поток 0 и поток 1 изменяют в массиве sumLocal разные, но следующие друг за другом элементы. В памяти эти элементы расположены рядом друг с другом и, следовательно, будут находиться в одной и той же строке кэша. Как показано на рис. 2, строка кэша загружается в кэш ЦП 0 и в кэш ЦП 1 (серые стрелки). Даже если эти потоки изменяют разные области памяти (красная и синяя стрелки), в целях поддержания когерентности кэша эта строка кэша объявляется недействительной на всех процессорах, в которые она загружена, что приводит к ее принудительному обновлению.

Рисунок 2. Ложное совместное использование (Ссылка: Avoiding and identifying false sharing among thread в разделе Ресурсы)
A block diagram depicting false sharing between thread 0 and thread 1.

Ложное совместное использование способно существенно ухудшить производительность приложения и трудно обнаруживается. Обратитесь к статье Memory issues on multi-core platform (автор: C.S. Liu), ссылка на которую приведена в разделе Ресурсы. В этой статье приведена простая программа, демонстрирующая отрицательные последствия ложного совместного использования.

Способы избежать ложного совместного использования

  • Ложного совместного использования можно избежать, согласуя структуры данных с границами строк кэша с помощью предоставляемых компилятором директив выравнивания, доступных при специальной компиляции для конкретного процессора. Например, на платформе Linux® заголовочный файл adm-i386/cache.h определяет макрос L1_CACHE_BYTES для размера строки в кэше L1 в архитектурах семейства Intel® x86. Размер строки кэша процессора также может быть задан программно. Обратитесь к разделу Ресурсы для получения дополнительной информации по согласованию структур данных с границами строк кэша и по кросс-платформенной функции для программного получения размера строки кэша.
  • Еще один метод — группировка часто используемых полей в структуре данных с целью их попадания в одну строку кэша, что позволяет загружать их за одно обращение к памяти. Это способствует уменьшению задержек памяти. Однако эта мера может увеличить объем кэша в случае очень больших структур данных и несколько снизить эффективности упаковки, направленной на ослабление или устранение ложного совместного использования. Обратитесь к статье под названием Elements of Cache Programming Style, ссылка на которую приведена в разделе Ресурсы.
  • Чтобы предотвратить ложное совместное использование в массивах, базис массива должен быть согласован с кэшем. Размер структуры должен быть целочисленным кратным или целочисленной долей размера строки кэша процессора.
  • Если необходимо принять какой-то фиксированный размер строки кэша для принудительного выравнивания, используйте величину 32 байта. Обратите внимание на следующие моменты.
    • Согласованные 32-байтовые строки кэша одновременно выровнены по границе 16-байт.
    • Для абсолютного большинство процессоров принятие строки кэша размером 32 байта является вполне адекватным решением. Например, процессор IBM PowerPC® 601 номинально имеет 64-байтовую строку кэша, однако в действительности она представляет собой две 32-байтовые строки кэша, соединенные последовательно. Процессор Sparc64 имеет 32-байтовую строку в кэше L1 и 64-байтовую строку в кэше L2. Процессор Alpha 32-байтовую строку в кэше L1. Процессор Itanium имеет 64-байтовую строку в кэше L1. Процессор IBM System z® имеет кэш L1 емкостью 256 K и 128-байтовые строки кэша. x86-процессоры имеют 64-байтовые строки в кэше L1.
    • Общее правило для эффективного исполнения на одном ядре – плотная упаковка данных с целью сокращения занимаемого ими места. Однако на многоядерном процессоре упаковка совместно используемых данных может породить серьезные издержки по причине ложного совместного использования. В общем случае решение состоит в том, чтобы плотно упаковать данные, предоставить каждому потоку его собственную частную копию для продолжения работы с этой копией, а затем объединить полученные результаты.
  • Дополняйте структуры или данные заполнителями до целых строк, чтобы данные, принадлежащие разным потокам или изменяемые разными потоками, гарантированно лежали в разных строках кэша.

Достаточно ли устранить ложное совместное использование? Нет!

В идеальном случае цель должна состоять в устранении любого совместного использования, а не только ложного совместного использования. Проектирование программного обеспечения должно быть направлено на исключение самой потребности в механизмах блокировки/синхронизации и вообще совместного использования. Прочтите статью Дмитрия Вьюкова (Dmitriy Vyukov), в которой этот вопрос рассматривается более детально.

Ложное совместное использование трудно обнаружить, однако имеется несколько инструментов, таких как Oprofile и модуль Data Race Detection (DRD) системы Valgrind, которые могут быть для этого полезны.


Устраните или ослабьте конкуренцию за право блокировки

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

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

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

Методы исключения конфликтов блокировок

  • Один из методов исключения конкуренции за право блокировки в структурах данных состоит в применении параллельных структур данных и свободных от блокировки (lock free) алгоритмов, которые избавляют от традиционных методов блокировки и синхронизации, таких как взаимное исключение (mutex). Для параллельных структур данных имеется несколько конструктов, не нуждающихся в использовании таких механизмов синхронизации, как взаимное исключение. В разделе Ресурсы приведены ссылки на несколько примеров таких параллельных конструктов для структур данных.
  • Некоторые примеры свободных от блокировки алгоритмов:
    • Масштабируемые параллельные хеш-таблицы (Scalable concurrent hash tables) с использованием технологии Relativistic Programming: Самой ранней реализацией этого подхода является механизм Read Copy Update (RCU), который широко применяется в ядре Linux. Это существенно повысило производительность и упростило код ядра Linux.
    • Неблокирующиеся расширяемые упорядоченные хеш-списки (Lock-free extensible Split-ordered Hash lists): Этот свободный от блокировок (lock-free) рекурсивный расширяемый алгоритм хеширования использует свободные от блокировок связанные списки, которые, в свою очередь, используют атомарные операции для модификации связанного списка.
  • В ядре Linux широко применяются "попроцессорные" переменные; другими словами, каждый процессор в системе получает свою собственную копию такой переменной. Доступ к попроцессорным переменным не нуждается в блокировке, а поскольку эти переменные не используются совместно потоками на разных процессорах, то не возникает ложного совместного использования или конфликтов памяти. Этот метод идеально подходит для сбора статистики.

Методы для ослабления конкуренции за право блокировки

  • При использовании традиционных методик блокировки или синхронизации, таких как спин-блокировка, необходимо позаботиться о том, чтобы не иметь монолитных или глобальных блокировок, а вместо этого разделить их на гранулированные фрагменты. В этом случае блокировки защищают лишь определенную и небольшую область структуры данных. Это позволяет нескольким потокам одновременно работать с различными элементами одной и той же структуры данных посредством установления соответствующей блокировки, защищающей эти элементы. Такой подход обеспечивает более высокий уровень параллелизма.
  • Даже если механизм синхронизации в программном конструкте улучшает параллелизм и ослабляет конфликты блокировки, могут иметь место проблемы производительности вследствие ложного совместного использования. В качестве примера рассмотрим структуру данных хеш-таблицы. При наличии массива спин-блокировок, защищающих каждый бакет (bucket) в хеш-таблице, в этом массиве спин-блокировок может возникать ложное совместное использование. Два потока (каждый из которых исполняется на своем процессоре), блокирующие разные бакеты в хеш-таблице, могут столкнуться с ложным совместным использованием, если получаемые ими спин-блокировки оказываются в одной и той же строки кэша. Таким образом, при проектировании таких алгоритмов необходимо применять методики общего характера для избежания ложного совместного использования.

Обнаружение конфликтов блокировок и устранение или ослабление конкуренции за право блокировки имеет большое значение для улучшения масштабируемости приложений в многоядерных многопроцессорных средах. Операционные системы предоставляют утилиты для выявления и оценки ограничений производительности вследствие конфликтов блокировок. Например, ОС Solaris предоставляет утилиту Lockstat для измерения конфликтов блокировок в модулях ядра. Ядро Linux предоставляет аналогичные утилиты Lockstat и Lockdep для выявления и оценки ограничений производительности вследствие конфликтов блокировок. Инструментарий Windows Performance Toolkit — Xperf обеспечивает аналогичные возможности в среде Windows. За дополнительной информацией обратитесь к разделу Ресурсы.


Избегайте конкуренции за кучу

Стандартные процедуры управления памятью языка C/C++ реализуются с использованием API-интерфейсов управления памятью для конкретных платформ. Как правило, эти процедуры базируются на концепции т. н. "кучи" (heap). Эти библиотечные процедуры (как однопоточные, так и многопоточные версии) выделяют или освобождают память в единственной куче. Куча — это глобальный ресурс, который совместно используют и за который конкурируют потоки определенного процесса. Такая конкуренция за кучу является одним из узких мест для многопоточных приложений с интенсивным использованием памяти.

Метод исключения конкуренции за кучу

  • Использовать для управления памятью локальную/частную кучу потока, благодаря чему устраняется конкуренция за ресурс. На платформе Windows выделенную кучу для каждого потока можно создать с помощью функции HeapCreate(), передав затем возвращенный дескриптор кучи в функцию HeapAlloc()/HeapFree().

В статье Memory issues on multi-core platform (автор C.S. Liu), ссылка на которую приведена в разделе Ресурсы, приведен пример конкуренции за кучу. Статья демонстрирует, что использование частной кучи повышает производительность примерно в три раза по сравнению с использованием глобальной кучи.

Примечание.

  • При создании кучи на платформе Windows можно активировать флаг heap_no_serialization. В этом случае при доступе к куче со стороны нескольких потоков не будет никаких издержек на синхронизацию. Однако выяснилась, что установка этого флага для частной кучи потока приводит к существенному замедлению работы в среде Vista и в более поздних операционных системах.
  • Флаг Heap_no_serialization и некоторые сценарии отладки деактивируют опцию Low Fragment Heap. Эта опция на сегодняшний день фактически является политикой по умолчанию для использования кучи и обеспечивает высокий уровень оптимизации.

Улучшайте привязку к процессору

Привязка к процессору (Processor affinity) — это атрибут потока или процесса, который говорит операционной системе, на каких ядрах или логических процессорах можно исполнять этот процесс. Этот атрибут чаще всего применяется при проектировании встроенного программного обеспечения.

На рис. 3 показана конфигурация системы с двумя процессорами, каждый из которых имеет по два ядра и общий кэш L2. В этой конфигурации поведение подсистемы кэширования для двух ядер на одном и том же процессоре и для двух ядер на разных процессорах будет различным. Если два связанных процесса или потока процесса назначаются двум ядрам на одном и том же процессоре, они могут лучше использовать общий кэш L2 и снизить издержки на поддержания когерентности кэша.

Рисунок 3. Система с несколькими ядрами и общей кэш-памятью L2
A block diagram depicting a multi-core, multiprocessor system with the cores on a processor sharing an L2 cache.

Рекомендации по проектированию

  • Проектировщики программного обеспечения для встроенных систем могут воспользоваться этими относительно низкими издержками на поддержание когерентности кэша, применив программное управление привязкой потоков к ядрам.
  • Следующие системные вызовы в операционных средах Linux и Windows сообщают приложению о том, что для определенного процесса задан атрибут Processor Affinity, а также устанавливают для процесса атрибут affinity mask.
    Листинг 2. Системные вызовы для управления атрибутом Processor Affinity
    Linux Example:
    /* Получить атрибут CPU affinity mask процесса */
    extern int 
    sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *cpuset);
    
    /* Задать атрибут affinity mask процесса */
    extern int 
    sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *cpuset);
    
    Windows Example:
    /* Задать атрибут Process Affinity */
    BOOL WINAPI 
    SetProcessAffinityMask(Handle hProcess, DWORD_PTR dwProcessAffinityMask);
    
    /* Задать атрибут Thread Affinity */
    DWORD_PTR WINAPI 
    SetThreadAffinityMask(Handle hThread, DWORD_PTR dwThreadAffinityMask);

Модели программирования

Проектировщик программного обеспечения может рассмотреть две различных модели программирования для назначения работы потокам в приложении, а именно:

  • Функциональная декомпозиция
    • Цель этой модели — понять, какие операции должно выполнять приложение или программное обеспечение, и присвоить каждую из этих операций отдельному потоку.
    • Несколько операции можно сгруппировать в рамках потока, однако результат функциональной декомпозиции состоит в том, что каждая операция выполняется определенным потоком.
  • Декомпозиция домена (или декомпозиция данных)
    • Цель этой модели — проанализировать наборы данных, которыми программное обеспечение или приложение должно управлять или которые должно обрабатывать, а затем осуществить их декомпозицию на менее крупные компоненты, которыми можно оперировать независимо друг от друга.
    • Потоки в программном обеспечении реплицируют операции, подлежащие исполнению, однако каждый поток работает с отдельным компонентом данных.

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

Вопросы, которые необходимо учитывать проектировщикам программного обеспечения

  • Очевидная простота взаимодействий кэша в модели с декомпозицией данных не означает, что эта модель безусловно предпочтительнее модели с функциональной декомпозицией; проблемы проектирования программного обеспечения, порождаемые поддержкой декомпозиции домена, могут оказаться весьма существенными.
  • Чтобы исключить возможность ложного совместного использования, данные в модели с декомпозицией по-прежнему должны находиться в разных строках кэша.

По ссылке Software Design Issues for Multi-core Multi-processor Systems (см. раздел Ресурсы) подробно описываются модели программирования, трудности их применения при проектировании программного обеспечения для многоядерных и многопроцессорных архитектур, а также преимущества этих моделей.


Заключение

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

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=AIX и UNIX
ArticleID=966329
ArticleTitle=Некоторые требования к проектированию программного обеспечения для многоядерных многопроцессорных архитектур
publish-date=03192014