| 23.11.2009 13:00 | |
При распараллеливании приложений на С/С++ часто оказывается, что функция malloc() (или new) является узким местом, ограничивающим максимальную производительность приложения. В данной статье объясняются четыре основные проблемы, которые может решить хороший параллельный механизм выделения памяти:
- Потокобезопасность
- Дополнительные издержки (Overhead)
- Соревновательность
- Дрейф памяти
Потокобезопасность
Базовые механизмы выделения памяти (storage allocators) не являются потокобезопасными, хотя в результате недавних усилий разработчиков данная проблема решается для многих параллельных платформ. В двух словах, из-за гонок во внутренних структурах данных механизма выделения памяти возникает вероятность неправильного поведения программы, когда два потока одновременно пытаются занять или освободить память. Если потоки имеют неограниченный доступ к механизму распределения памяти, как показано ниже, то они начинают «наступать друг другу на пятки», что приводит к ненормальному поведению системы.
Простым решением данной проблемы является применение мьютексной блокировки (mutex, mutual exclusion, взаимное исключение) к аллокатору перед вызовом malloc() или free(), что позволяет только одному потоку получить доступ к внутренним структурам данных механизма выделения памяти за раз.
Если механизм выделения памяти является потокобезопасным, блокирующий протокол встраивается непосредственно в его логику.
Издержки и соревнование
У потокобезопасных механизмов выделения памяти с использованием блокировок возможны две проблемы: во-первых, выделение и освобождение памяти могут замедляться вследствие дополнительных издержек на установку блокировки. Во-вторых, при доступе к аллокатору может возникнуть соревнование, что приводит к замедлению работы приложения и ограничению его масштабируемости. Соревнование само по себе не является большой проблемой при 2 или 4 ядрах, но, по закону Мура, скоро можно ожидать десятков и даже сотен ядер в одном процессоре, и тогда «соревновательность» может сильно подкосить масштабируемость.
Обе проблемы можно решить с помощью распределенного механизма выделения памяти, который создает локальный пул памяти при каждом потоке, как это показано на рисунке.
Распределенный аллокатор позволяет выделять и освобождать память в основном в пределах локальных пулов. В редких случаях, когда локальный пул истощается, поток может получить дополнительную память (большими кусками), из глобального пула. Таким образом, решается проблема соревновательности, поскольку потоки редко обращаются к глобальному пулу. Решается также и проблема издержек, потому что при обращении к локальным пулам блокировки не требуются.
Дрейф памяти
К сожалению, использование локальных потоков создает другую проблему, особенно на платформах, в которых память активно распределяется между потоками, или в которых происходит активное перераспределение вычислительной нагрузки между потоками. Представим, что поток А постоянно требует выделения памяти в своем локальном пуле и затем передает полученную память потоку Б, который использует ее в своем локальном пуле. Когда пул потока А истощается, он запрашивает дополнительную память из глобального пула. Эта память снова передается потоку Б, и он опять же использует ее в своем локальном пуле. С течением времени локальный пул памяти потока Б неограниченно разрастается, создавая что-то вроде утечки памяти, при которой объем виртуальной памяти приложения все время растет.
Этот дрейф памяти можно разрешить двумя путями. Одно решение состоит в том, что поток, чей локальный пул становится слишком большим, отдает часть памяти глобальному пулу. Другое – все потоки, освобождая память, возвращают ее тому пулу, из которого она была отведена. Оба этих метода можно реализовать с низкими издержками, и они оба дают удовлетворительное решение проблемы дрейфа памяти.
Выводы
Существуют и другие проблемы, возникающие в параллельных механизмах выделения памяти. Например, ложное общее использование памяти (false sharing) является особенно разрушительным состоянием, при котором два потока обращаются к независимым блокам памяти, которые случайно оказываются на одной и той же линии кэша, что ведет к перегрузке протокола когерентности кэша в процессоре. Аллокатор памяти, не уделяющий должного внимания границам линий кэша и раздающий разным потокам блоки памяти, лежащие на одной линии, может вызывать ложное разделение, которое трудно отловить, поскольку логика кода говорит, что потоки обращаются к независимым блокам памяти.
Можно привести два примера параллельных отводчиков памяти – это Hoard, написанный Эмери Бергером (Emery Berger) из университета Массачусетса, и Miser, распространяемый Cilk Arts как часть пакета Cilk++.
Дополнительные ресурсы по Cilk++
- Скачать Intel® Cilk++ SDK
- Miser – динамически загружаемый аллокатор памяти для многопоточных приложений
- Визуализация «параллельного ускорения» с помощью Cilkview
- Четыре причины, почему параллельные программы должны иметь последовательную семантику
Данная статья является переводом блога Чарльза Лизерсона (Charles Leiserson).
Оригинал доступен по адресу http://www.cilk.com/multicore-blog/bid/7904/Multicore-Storage-Allocation.
Пожалуйста, обратитесь к странице Уведомление об оптимизации для более подробной информации относительно производительности и оптимизации в программных продуктах компании Intel.
Комментарии (0) 
Обратная ссылка (2)
- Блоги Intel® Software Network » Суров закон параллелизма, или «что за #@%&^ этот ваш параллелизм?»
11.12.2009 04:31 - Блог компании Intel Corporation / Статьи на ISN
08.10.2010 01:53

