В параллельном коде OpenMP, будет ли какая-то польза для параллельного запуска memset?

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

2 ответа

Решение

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

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

Четырехъядерный процессор Intel Xeon X7350 - четырехядерный процессор до 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

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

Один поток уже насыщает пропускную способность памяти одной ссылки ЦП <-> 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, где выполняется поток, который первым касается виртуального адреса на определенной странице. Закрепление потоков (привязка к конкретным ядрам процессора) имеет важное значение в таких системах, поскольку миграция потоков приводит к удаленному доступу, который медленнее. Поддерживается для pinnig доступно в большинстве сред выполнения OpenMP. GCC со своим libgomp имеет GOMP_CPU_AFFINITY переменная среды, Intel имеет KMP_AFFINITY переменная окружения и т. д. Кроме того, OpenMP 4.0 представил независимую от производителя концепцию мест.

Изменить: Для полноты, вот результаты запуска кода с массивом 1 ГБ на MacBook Air с Intel Core i5-2557M (двухъядерный процессор Sandy Bridge с 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 байта за раз и, вероятно, насытить ссылку памяти.

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

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

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

Другие вопросы по тегам