В параллельном коде OpenMP будет ли какая-либо выгода для параллельной работы memset?

У меня есть блоки памяти, которые могут быть довольно большими (больше, чем кэш L2), и иногда я должен установить их на все ноль. memset хорош в серийном коде, но как насчет параллельного кода? Кто-нибудь испытывает, если вызов memset из параллельных потоков фактически ускоряет работу для больших массивов? Или даже используя простой openmp-параллель для циклов?

Ответ 1

Люди из HPC обычно говорят, что одного потока обычно недостаточно для насыщения одной ссылки на память, что обычно верно для сетевых ссылок. Здесь - это быстрый и грязный memsetter с поддержкой OpenMP, который я написал для вас, который заполняет нулями дважды 2 гигабайта памяти. И вот результаты с использованием GCC 4.7 с различным количеством потоков на разных архитектурах (максимальные значения из нескольких запущенных отчетов):

GCC 4.7, код, скомпилированный с помощью -O3 -mtune=native -fopenmp:

Четырехъядерный Intel Xeon X7350 - четырехъядерный процессор pre-Nehalem с отдельным контроллером памяти и шиной на передней панели

одиночный разъем

threads   1st touch      rewrite
1         1452.223 MB/s  3279.745 MB/s
2         1541.130 MB/s  3227.216 MB/s
3         1502.889 MB/s  3215.992 MB/s
4         1468.931 MB/s  3201.481 MB/s

(1-е касание выполняется медленно, так как команда нитей создается с нуля, а операционная система отображает физические страницы в виртуальное адресное пространство, зарезервированное malloc(3))

Один поток уже насыщает полосу пропускания памяти на одном канале CPU ↔ NB. (NB = Северный мост)

1 поток на каждый сокет

threads   1st touch      rewrite
1         1455.603 MB/s  3273.959 MB/s
2         2824.883 MB/s  5346.416 MB/s
3         3979.515 MB/s  5301.140 MB/s
4         4128.784 MB/s  5296.082 MB/s

Два потока необходимы для насыщения полной полосы пропускания памяти канала NB ↔ .

Octo-socket Intel Xeon X7550 - 8-канальная система NUMA с окто-ядерными процессорами (CMT отключена)

одиночный разъем

threads   1st touch      rewrite
1         1469.897 MB/s  3435.087 MB/s
2         2801.953 MB/s  6527.076 MB/s
3         3805.691 MB/s  9297.412 MB/s
4         4647.067 MB/s  10816.266 MB/s
5         5159.968 MB/s  11220.991 MB/s
6         5330.690 MB/s  11227.760 MB/s

Для насыщения полосы пропускания одной линии памяти требуется не менее 5 потоков.

1 поток на каждый сокет

threads   1st touch      rewrite
1         1460.012 MB/s  3436.950 MB/s
2         2928.678 MB/s  6866.857 MB/s
3         4408.359 MB/s  10301.129 MB/s
4         5859.548 MB/s  13712.755 MB/s
5         7276.209 MB/s  16940.793 MB/s
6         8760.900 MB/s  20252.937 MB/s

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

Основная проблема в системах NUMA - политика памяти первого касания - память распределяется по NUMA node, где выполняется поток, который первым коснется виртуальный адрес на определенной странице. Фиксирование нити (привязка к конкретным ядрам ЦП) имеет важное значение для таких систем, поскольку миграция потоков приводит к удаленному доступу, который медленнее. Поддержка pinnig доступна в большинстве случаев работы OpenMP. GCC с его libgomp имеет переменную окружения GOMP_CPU_AFFINITY, у Intel есть переменная среды KMP_AFFINITY и т.д. Также OpenMP 4.0 представил концептуальную концепцию местоположений, не зависящую от поставщика.

Изменить:. Для полноты здесь приведены результаты запуска кода с массивом 1 GiB на MacBook Air с Intel Core i5-2557M (двухъядерный Sandy Bridge CPU с HT и QPI). Компилятор - это GCC 4.2.1 (сборка Apple LLVM)

threads   1st touch      rewrite
1         2257.699 MB/s  7659.678 MB/s
2         3282.500 MB/s  8157.528 MB/s
3         4109.371 MB/s  8157.335 MB/s
4         4591.780 MB/s  8141.439 MB/s

Почему эта высокая скорость даже с одним потоком? Небольшое исследование с gdb показывает, что memset(buf, 0, len) переводится компилятором OS X на bzero(buf, len) и что версия с поддержкой SSE4.2 с именем bzero$VARIANT$sse42 предоставляется libc.dylib и используется при запуске - время. Он использует команду MOVDQA для нуля 16 байт памяти одновременно. Вот почему даже с одним потоком пропускная способность памяти почти насыщена. Однопотоковая версия с поддержкой AVX с использованием VMOVDQA может сразу же обнулить 32 байта и, возможно, насытить ссылку на память.

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

Ответ 2

Ну, всегда есть кеш L3...

Однако очень вероятно, что это будет связано уже с пропускной способностью основной памяти; добавление большего количества parallelism вряд ли улучшит ситуацию.