Резюме: Как можно указать в своем коде, что OpenMP должен использовать потоки только для НАСТОЯЩИХ ядер, т.е. не учитывать гиперпоточность?
Подробный анализ. На протяжении многих лет в свободное время я писал программный рендерер с открытым исходным кодом (растеризатор/трассировщик лучей). Код GPL и двоичные файлы Windows доступны здесь: https://www.thanassis.space/renderer.html Он компилируется и отлично работает под Windows, Linux, OS/X и BSD.
В прошлом месяце я представил режим трассировки лучей, и качество сгенерированных изображений взлетело до небес. К сожалению, трассировка лучей на несколько порядков медленнее, чем растеризация. Чтобы увеличить скорость, как и для растеризаторов, я добавил поддержку OpenMP (и TBB) в трассировщик лучей, чтобы легко использовать дополнительные ядра ЦП. И растеризация, и трассировка лучей легко поддаются многопоточности (работа на треугольник — работа на пиксель).
Дома, с моим Core2Duo, 2-е ядро помогло во всех режимах — и режимы растеризации, и режимы трассировки лучей получили ускорение от 1,85x до 1,9x.
Проблема: Естественно, мне было любопытно увидеть максимальную производительность ЦП (я также «играю» с графическими процессорами, предварительный порт CUDA), поэтому мне нужна была надежная база для сравнения. Я дал код своему хорошему другу, у которого есть доступ к «звериной» машине с 16-ядерным суперпроцессором Intel за 1500 долларов.
Он запускает его в «самом тяжелом» режиме, в режиме трассировки лучей...
...и он в пять раз быстрее моего Core2Duo (!)
Вздох - ужас. Что только что произошло?
Начали пробовать разные модификации, патчи,... и в итоге разобрались.
Используя переменную среды OMP_NUM_THREADS, можно контролировать количество создаваемых потоков OpenMP. По мере увеличения количества потоков с 1 до 8 скорость увеличивалась (близка к линейному увеличению). В тот момент, когда мы пересекли 8, скорость начала уменьшаться, пока не упала до одной пятой скорости моего Core2Duo, когда были задействованы все 16 ядер!
Почему 8?
Потому что 8 — это количество настоящих ядер. Остальные 8 были... гиперпотоковыми!
Теория. Для меня это было новостью — я видел, что гиперпоточность сильно помогает (до 25%) в других алгоритмах, так что это было неожиданно. Судя по всему, несмотря на то, что каждое ядро гиперпоточности имеет свои собственные регистры (и модуль SSE?), трассировщик лучей не мог использовать дополнительную вычислительную мощность. Что заставило меня задуматься...
Вероятно, голодает не вычислительная мощность, а пропускная способность памяти.
Трассировщик лучей использует структуру данных иерархии ограничивающих объемов для ускорения пересечения лучей и треугольников. Если используются ядра с гиперпоточностью, то каждое из «логических ядер» в паре пытается читать из разных мест в этой структуре данных (то есть в памяти) — и кэши ЦП (локальные для пары) полностью перегружены. По крайней мере, это моя теория - любые предложения приветствуются.
Итак, вопрос: OpenMP определяет количество «ядер» и порождает потоки, соответствующие ему, то есть включает в расчет «ядра» с гиперпоточностью. В моем случае это, по-видимому, приводит к катастрофическим результатам с точки зрения скорости. Кто-нибудь знает, как использовать OpenMP API (если возможно, переносимо), чтобы создавать потоки только для НАСТОЯЩИХ ядер, а не для гиперпоточных?
P.S. Код открыт (GPL) и доступен по ссылке выше, не стесняйтесь воспроизводить его на своей машине — я предполагаю, что это произойдет во всех процессорах с гиперпоточностью.
П.П.С. Извините за длину поста, я подумал, что это образовательный опыт и хотел поделиться.