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

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


person nat chouf    schedule 20.07.2012    source источник
comment
Навряд ли. memset при выходе данных из кэша, скорее всего, будет ограничено пропускной способностью памяти.   -  person Mysticial    schedule 20.07.2012
comment
Параллельный запуск memset на машине NUMA (и все системы Intel с процессором MP после Core2, а также все системы MP и даже некоторые системы AMD UP имеют NUMA) может быть самым трудным для понимания убийцей производительности, если только не позже одни и те же потоки будут обращаться только к тем частям массива, которые они лично обнулили.   -  person Hristo Iliev    schedule 20.07.2012
comment
Тем не менее, существует отраслевой стандартный тест STREAM. Возьмите версию OpenMP, скомпилируйте и запустите с другой количество потоков, чтобы убедиться в этом. Также обратите внимание, что memset() поддерживает SIMD в большинстве libc реализаций и уже увеличивает пропускную способность памяти до максимума.   -  person Hristo Iliev    schedule 20.07.2012


Ответы (2)


Специалисты по высокопроизводительным вычислениям обычно говорят, что одного потока обычно недостаточно для насыщения одного канала памяти, то же самое обычно справедливо и для сетевых ссылок. Вот быстрый и грязный memsetter с поддержкой OpenMP, который я написал для вас, который дважды заполняет нулями 2 ГиБ памяти. И вот результаты использования 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

(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 с 8-ядерными процессорами (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 байта и, вероятно, переполнить связь с памятью.

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

person Hristo Iliev    schedule 20.07.2012
comment
Спасибо за эти результаты. Как вы контролируете 1 поток / сокет или все потоки в 1 сокете? - person nat chouf; 21.07.2012
comment
С taskset и / или установкой переменной GOMP_CPU_AFFINITY. Если у вас установлен hwloc, он предоставляет отличный hwloc-ls инструмент. Просто запустите его как hwloc-ls --taskset, и он покажет вам необходимую битовую маску для taskset, например. работать на одной розетке. - person Hristo Iliev; 21.07.2012
comment
Это отличный ответ. Но не могли бы вы объяснить, почему существует такая разница между первым прикосновением и перезаписью? Я не совсем понимаю, что вы имеете в виду под 1-м прикосновением, медленным, поскольку команда потоков создается с нуля, а операционная система отображает физические страницы в виртуальное адресное пространство, зарезервированное malloc (3) - person Z boson; 23.08.2014
comment
@Zboson, при первом вызове malloc память распределяется с использованием анонимного mmap. Это приводит к отображению в виртуальном адресном пространстве процесса, но это отображение по-прежнему не поддерживается физическими фреймами RAM, а специальная страница ядра со всеми нулями отображается везде в пределах области для копирования при записи. Следовательно, чтение из недавно созданной памяти, созданной в формате mmap, возвращает нули. При первой записи на некоторый адрес в этой области происходит сбой страницы, обработчик сбоев находит свободный кадр RAM и сопоставляет его с соответствующей страницей. - person Hristo Iliev; 23.08.2014
comment
Можно уменьшить накладные расходы на первое касание, запросив использование огромных страниц или указав mmap(2) предоставить память с предварительным сбоями (в Linux от MAP_POPULATE; OS X не поддерживает предварительный сброс). Во втором случае вызов mmap будет очень медленным, но не будет никакой разницы в доступе к памяти между первым касанием и перезаписью. - person Hristo Iliev; 23.08.2014
comment
@HristoIliev, спасибо, в этом больше смысла. Я не знал mmap. Мне еще многому нужно научиться. - person Z boson; 23.08.2014
comment
Я вижу что-то очень странное. Когда я привязываю потоки и устанавливаю количество потоков равным количеству физических ядер, прикосновение происходит быстрее! Например, в моей 4-ядерной системе Ivy Bridge system (8 HT) с Linux и GCC я использую export OMP_NUM_THREADS=4 и export OMP_PROC_BIND=true. Получаю Touch: 21191,013 МБ / с. Переписываем: 18112,064 МБ / с. Но без привязки и использования восьми потоков получаю Touch: 11830,490 МБ / с. Rewrite: 17933,885 МБ / с. Для меня это не имеет смысла. - person Z boson; 25.08.2014
comment
stackoverflow.com/questions/25483363/ - person Z boson; 25.08.2014

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

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

person Oliver Charlesworth    schedule 20.07.2012