У меня есть блоки памяти, которые могут быть довольно большими (больше, чем кеш L2), и иногда мне приходится устанавливать их все на ноль. memset хорош в последовательном коде, но как насчет параллельного кода? Есть ли у кого-нибудь опыт, действительно ли вызов memset из параллельных потоков ускоряет работу для больших массивов? Или даже использовать простой параллельный цикл openmp для циклов?
Будет ли какая-то польза от параллельного запуска memset в параллельном коде OpenMP?
Ответы (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 байта и, вероятно, переполнить связь с памятью.
Важное сообщение здесь заключается в том, что иногда векторизация и многопоточность не являются ортогональными в ускорении операции.
taskset
и / или установкой переменной GOMP_CPU_AFFINITY
. Если у вас установлен hwloc
, он предоставляет отличный hwloc-ls
инструмент. Просто запустите его как hwloc-ls --taskset
, и он покажет вам необходимую битовую маску для taskset
, например. работать на одной розетке.
- person Hristo Iliev; 21.07.2012
malloc
память распределяется с использованием анонимного mmap
. Это приводит к отображению в виртуальном адресном пространстве процесса, но это отображение по-прежнему не поддерживается физическими фреймами RAM, а специальная страница ядра со всеми нулями отображается везде в пределах области для копирования при записи. Следовательно, чтение из недавно созданной памяти, созданной в формате mmap, возвращает нули. При первой записи на некоторый адрес в этой области происходит сбой страницы, обработчик сбоев находит свободный кадр RAM и сопоставляет его с соответствующей страницей.
- person Hristo Iliev; 23.08.2014
mmap(2)
предоставить память с предварительным сбоями (в Linux от MAP_POPULATE
; OS X не поддерживает предварительный сброс). Во втором случае вызов mmap
будет очень медленным, но не будет никакой разницы в доступе к памяти между первым касанием и перезаписью.
- person Hristo Iliev; 23.08.2014
mmap
. Мне еще многому нужно научиться.
- person Z boson; 23.08.2014
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
Ну, кеш L3 всегда есть ...
Однако весьма вероятно, что это уже будет связано с пропускной способностью основной памяти; добавление большего количества параллелизма вряд ли улучшит ситуацию.
memset
при выходе данных из кэша, скорее всего, будет ограничено пропускной способностью памяти. - person Mysticial   schedule 20.07.2012memset
на машине NUMA (и все системы Intel с процессором MP после Core2, а также все системы MP и даже некоторые системы AMD UP имеют NUMA) может быть самым трудным для понимания убийцей производительности, если только не позже одни и те же потоки будут обращаться только к тем частям массива, которые они лично обнулили. - person Hristo Iliev   schedule 20.07.2012memset()
поддерживает SIMD в большинствеlibc
реализаций и уже увеличивает пропускную способность памяти до максимума. - person Hristo Iliev   schedule 20.07.2012