Как стать автором
Обновить

Комментарии 38

Хороший мультфильм, до сих пор не потерял своей актуальности и смотрибельности. Касается ли это Фортрана… Почему бы и нет.

При датасете 1000х1000 интов общий размер массива примерно 4Мб для 32-битных интов и 8Мб для 64-хбитных. О какой работе с памятью идет речь, если это все помещается в кэш? Да и считать там мягко говоря нечего, о чем косвенно говорит время работы замеряемого цикла.

Замер на сервере вашего примера дает 2 сек для автопараллельного варианта и 6 секунд для последовательного. Что дает 3х ускорение, вместо Nx, где N - количество ядер (синхронизацией пренебрегаем в первом приближении).
Теперь смотрите за руками. Увеличиваем датасет в 4 раза (2000х2000). Получаем:
27сек - последовательный вариант;
3 сек - автопараллелизатор;
3 сек - OpenMP.
Что в принципе ожидаемо - автопараллелизатор это и так автоматическое распараллеливание на OpenMP. А ускорение? Уже почти 10х. Что это нам говорит? Это нам говорит что бенчмарк не репрезентативен. На мелком датасете можо измерять краевые эффекты от работы кеша, но не более. Ни о качестве кода, ни о качестве распараллеливания это нам ничего не говорит.

Вообще-то при написании реальных программ как раз надо стремиться написать алгоритм таким образом, чтобы обрабатывать данные порциями, более-менее умещающимися в кеш, поэтому бенчмарк вполне репрезентативен. Размер массива в тесте выбран таким образом, чтобы не делать лишних настроек в Linux, связанных со стеком OpenMP.

К сожалению, вы не указываете на каком процессоре вы получили свой результат (очевидно, с помощью компилятора ifort).

На моём железе (Xeon 4C E3-1270v2 @ 3.9 GHz, 8 ядер, кеш 8М) получаются такие результаты с предложенными вами датасетом 2000*2000 и размером элемента 4 байта:

Xeon ifort последовательный: 74 сек

Xeon ifort автопараллельный: 42 сек

Xeon ifort OpenMP: 40 сек

Что вполне соответствует показанному в статье результату. Узким местом в данных условиях по-прежнему является оперативная память.

Ваш результат можно будет объяснить, когда вы напишете, в каком окружении его получили. Скорее всего, вы это делаете под Windows, где время исполнения программ порядка 1-2 секунд вообще сильно подвержено различным флуктуациям. Также видно, что у вас установлена быстрая относительно вашего процессора оперативная память. На одновременный доступ к памяти 10 ядер способна не каждая конфигурация.

Вообще-то при написании реальных программ как раз надо стремиться написать алгоритм таким образом, чтобы обрабатывать данные порциями, более-менее умещающимися в кеш, поэтому бенчмарк вполне репрезентативен. Размер массива в тесте выбран таким образом, чтобы не делать лишних настроек в Linux, связанных со стеком OpenMP.

Ну тогда зачем вы пишете что у вас "расшивка параллельных процессов по адресам памяти ускоряет работу с оперативной памятью" если в память вы не ходите, а все сидит в кэше? Если вам настройки OpenMP мешают ну... Ну тогда что вы меряете? Считать там нечего, в память бенчмарк не ходит.

К сожалению, вы не указываете на каком процессоре вы получили свой результат (очевидно, с помощью компилятора ifort).

Intel Fortran (ifort) OneAPI2023.0. Тестовая машина: IceLake 8380 Processor 2x40 cores, 60M Cache, 2.30 GHz; 256Gb DDR4. И то что я получил 10х ускорения это вообщем то ни о чем. У вас тест без зависимостей, там только закольцовка последовательная, и ее можно тоже распараллелить. А это значит что тест должен масштабироваться линейно, с увеличением количества используемых ядер. Я еще 4х где то потерял.

Ваши догадки насчет окружения я пожалуй пропущу мимо ушей. Если вы пишете бенчмарк, то придерживайтесь хотя бы здравого смысла. А он, лично мне, говорит о том что ваш вывод - неверен.
Если сравнить OpenMP версию без зависимостей и coarray где вам нужно еще склеивать границы, то во втором дополнительной работы больше, и он должен быть медленнее. Будет время я промеряю ее на увеличенном размере, т.к. на размере L2 кэша особого смысла замерять нет. Да и характерное время работы бенчмарка должно быть больше погрешности измерения. А в коде дискретность таймера 1 секунда.

Ну тогда зачем вы пишете что у вас "расшивка параллельных процессов по адресам памяти ускоряет работу с оперативной памятью" если в память вы не ходите, а все сидит в кэше?

Так это в конечном итоге относится как раз к кешу.

А он, лично мне, говорит о том что ваш вывод - неверен. 

Ну померяйте по-своему и получите какие-нибудь другие результаты. Пока вы просто запустили программу на более быстрой машине, которой, естественно, нужно больше работы для информативного сравнения. Именно на этот случай в тестовой программе предусмотрены настройки размеров.

На 40 ядер вы в любом случае вряд ли сможете смасштабировать smp код, вот ваши 4x и потерялись.

Да и характерное время работы бенчмарка должно быть больше погрешности измерения. А в коде дискретность таймера 1 секунда.

Дискретность таймера меньше одной секунды, просто общее время работы выводится в секундах. Для более дискретной оценки производительности смотрите количество операций в секунду.

На 40 ядер вы в любом случае вряд ли сможете смасштабировать smp код

Что этому помешает?

Накладные расходы на поддержание когерентности кеша между ядрами.

Ну тоесть если я сделаю 39х ускорение на этом коде (немного отложим на хвосты) - вы возьмете свои слова обратно? :) При этом естесственно я могу изменять размер задачи.

В смысле какие слова? Мне такой результат представляется маловероятным для smp кода при невырожденных размерах массива.

Какие-какие... Про невозможность 40х и когерентность кешей как причину недостижимости

Ну я не говорил про невозможность. Именно число 40 не записано ни в каких божественных скрижалях. Это возможно, если у вас кеш на один-два порядка быстрее АЛУ. Для современного состояния вычислительной техники такое сомнительно.

Я напомню что мы меряем не абсолютную скорость, а относительную -параллельный код против последовательного. И в последовательном варианте будет тот же медленый алу (как вы утверждаете) против быстрого кэша.

Не очень понял логику вашего рассуждения. Памяти сложнее на себе тащить 40 устройств выборки, чем одно такое же. Как бы эта память внутри не была устроена. Но это всё проще взять и померить.

Ну пожалста:

export OMP_NUM_THREADS=40

rm -f *.bin
rm -f *.txt

ifort -fpp -O3 -xAVX2 ./life_serial.f90 -o ./life_serial.bin -DFOUT='life_serial.txt'
./life_serial.bin

export KMP_AFFINITY=scatter,granularity=fine
ifort -fpp -O3 -xAVX2 -parallel -fopenmp ./life_openmp.f90 -o ./life_openmp.bin -DFOUT='life_openmp.txt'
./life_openmp.bin

               195 сек,              820000000 ячеек/с
                 5 сек,            28466000000 ячеек/с

Отличный результат, 34.7x. А что в определениях?

И посмотрите, пожалуйста, производительность mpp версии при тех же параметрах на этой машине.

6кх6к размер задачи. Переписанная инициализация на параллельный цикл, чтобы убрать нума-эффекты. Пиннинг процессов.
Теперь вопрос, а как coarrays помогут нам разогнать кэш? Раз дело в нем?

6кх6к размер задачи

Чему равен kind?

Теперь вопрос, а как coarrays помогут нам разогнать кэш? Раз дело в нем?

Coarrays помогут нам немного уменьшить количество конфликтов между разными ядрами при доступе к одним и тем же значениям в кеше. Хотя это, конечно, не то, зачем они задуманы.

Kind массива данных не менялся. Типы счетчиков времени и переменные границ увеличены до 8 байт, иначе считало отрицательное "ячеек/с", что как бы не комильфо.

Kind массива данных не менялся

То есть 1?

Если подумать, то больший размер задачи (и, соответственно, кеша) уменьшает вероятность совпадения одновременно обрабатываемых адресов между различными ядрами и, соответственно, конфликта. Так как у вас размер массива в 36 раз больше чем у меня, размер кеша в 30 раз больше, а ядер всего в 5 раз больше, то это благостно для производительности. Возвращаясь к исходной размерности, получаете исходную масштабируемость.

Coarrays помогут нам немного уменьшить количество конфликтов между разными ядрами при доступе к одним и тем же значениям в кеше.

Чуть менее чем никак. Те же ядра, те же кэши. От того что данные у вас не в потоке, а в отдельном процессе ничего не поменяется.

Логическая структура памяти же другая. Каждый процесс почти полностью работает с данными в своём собственном адресном пространстве, поэтому у разных ядер не возникает конфликтов по доступу к одним и тем же ячейкам кеша.

Ваши виртуальные адреса кэшу не интересны, он работает с физическими, для него нет разницы - один процесс обращается к памяти или разные.
Да и какие там конфликты? Если бы у вас был один массив, из которого вы читали и в который же писали - тогда да. Атомики или синхронизация нужна. А так у вас из одной кучи читается, в другую пишется, никто из потоков не может перезаписать уже записанную ячейку. Конфликтов 0.

Так физические адреса, с которыми работают процессы, тоже разные.

А так у вас из одной кучи читается, в другую пишется, никто из потоков не может перезаписать уже записанную ячейку. Конфликтов 0.

Логического конфликта нет, но схемотехнически параллельный доступ на чтение тоже является конфликтной ситуацией. В конечном итоге ток течёт строго из точки А в точку Б.

>> но схемотехнически параллельный доступ на чтение тоже является конфликтной ситуацией
С чего бы это? Да и не забывайте что L1 и L2 у каждого ядра - свой.

Извините, ошибся уровнем комментариев О_о

С чего бы это?

Именно с того, что ток, соответствующий сигналу в цифровых схемах, всегда течёт только между двумя точками. Для оптимизации таких случаев делается многопортовая память (оперативная или L3), но всё имеет свои пределы и стоимость.

Да и не забывайте что L1 и L2 у каждого ядра - свой.

Поэтому их приходится синхронизировать между собой, когда они содержат копии одних и тех же ячеек памяти. Когда ядро 1 читает элемент массива из своего кеша L1, то надо сначала убедиться, что ни одно из других ядер не записало новое значение этого элемента в свой кеш L1.

ток, соответствующий сигналу в цифровых схемах, всегда течёт только между двумя точками

Прощайте шинные и параллельные схемы. Если вы возьмете какую нибудь мелкую микросхему (например народной 155 или любой другой серии), то внезапно вы обнаружите что там есть "нагрузочная способность" или коэффициент разветвления. Он как раз показывает сколько входов можно подключить к одному выходу. Так что две точки - это ваши фантазии.

Когда ядро 1 читает элемент массива из своего кеша L1, то надо сначала убедиться, что ни одно из других ядер не записало новое значение этого элемента в свой кеш L1.

Тоесть в вашем представлении каждое чтение из памяти предваряется обходом всех ядер процессора с вопросом - "слышь мужик, ты вот сюда ничего не писал?"
Нет, это не так работает. Вики, MESI и так далее.

На шине обычно все устройства, кроме двух, находятся в z-состоянии. За редкими исключениями, не относящимися к обсуждаемому вопросу. Но два ядра не могут одновременно выставлять адреса ячеек памяти на одном порту.

Тоесть в вашем представлении каждое чтение из памяти предваряется обходом всех ядер процессора с вопросом - "слышь мужик, ты вот сюда ничего не писал?" 

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

Для ARM и POWER ситуация в схемотехнике немного попроще в этом отношении, так как там более слабая модель памяти, но для Intel проблема синхронизации чтений очень важна.

На шине обычно все устройства, кроме двух, находятся в z-состоянии

Т.е. ситуации когда один передатчик и множество приемников - не бывает? Ну ну..

Но тем не менее, оверхед есть, и вы его наглядно видите.

Где я его вижу? Оверхеда по чтению нет. Оверхед идет по записи, и то довольно сильно ограничен снуп-кэшами.

Закомментируйте ветку case default, и у вас записи не будет, а оверхед будет, причём ещё больше.

Ну, надо в прологе программы тогда весь массив обнулить, понятно.

Казалось бы, избавили программу от огромной массы излишних операций записи и больше ничего не поменяли, а её выполнение от этого замедлилось. Вот так вот бывает.

И компилятор обмануть, который увидит что результат никуда не сохраняется и пройдется по всему этому dead code ellimination

Компилятор не знает, по какой ветке у нас в каждом случае пойдёт case, и поэтому сохранится ли результат. Он предсказывать поведение клеточного автомата не может, чем и хороша задача.

Хорошо, сделаю. Ваш прогноз - какую цифру мы увидим?

Да бог его знает. Я бы ожидал в районе 20 миллиардов omp, а seq без изменений.

Пока IceLake8380 не доступен, промерял на IceLake8360Y.
OMP_NUM_THREADS=40, AVX2:
serial: 228 сек, 699000000 ячеек/с
serial_nowrt: 374 сек, 427000000 ячеек/с
openmp: 6 сек, 26087000000 ячеек/с
openmp_nowrt: 11 сек, 14517000000 ячеек/с


Без default медленнее.Case для nowrt выглядит так:

  select case (s)
    case (3)
      m2 (i, j) = 1
    case (2)
      m2 (i, j) = m1 (i, j)
    case default
!          m2 (i, j) = 0 
end select

Вроде бы разгрузили цикл, убрали частую запись, а стало хуже. Как так?
А теперь правильный ответ.

После того, как мы убрали default компилятор решил что цикл стал слишком простым для векторизации и не заходел ее делать. Но у нас есть стредства. Добавим -vec-threshold0 в ключи. И еще уберем точки. Вообще, т.к. чистить их теперь некому, а если не чистить то поле быстро забивается точками и default перестает работать.

serial: 230 сек, 692000000 ячеек/с
serial_nowrt: 204 сек, 780000000 ячеек/с

Ну вот, теперь другое дело.
С openmp_nowrt повожусь попожзе, там внезапно возникает куча точек :)

Разобрался:

 openmp:          6 сек,            24880000000 ячеек/с
 openmp_nowrt:    6 сек,            26471000000 ячеек/с

¯\_(ツ)_/¯

Как бы то ни было, записей нет, а оверхед остаётся, что я изначально и хотел продемонстрировать.

И еще уберем точки. Вообще, т.к. чистить их теперь некому

Если в самом начале обнулить весь массив оператором field=0, то их не надо чистить, так как весь движ будет происходить с несколькими точками в окрестностях “мигалки”, которые имеют либо по 2-3 соседа (что охватывается оставшимися ветками case), либо их период (равный 2) совпадает с количеством сечений массива. В общем, если внимательно всё посмотреть, то ничего плохого конкретно в нашей конфигурации не происходит, везде всё совпадает нужным образом.

Хотя у интела может кодогенератор поумнее в этом отношении, наставит там каких-нибудь фиктивных команд записи. Но оверхед от чтений всё равно останется.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории