Гиперпоточность сделала мой рендерер в 10 раз медленнее

Резюме: Как можно указать в своем коде, что 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) и доступен по ссылке выше, не стесняйтесь воспроизводить его на своей машине — я предполагаю, что это произойдет во всех процессорах с гиперпоточностью.

П.П.С. Извините за длину поста, я подумал, что это образовательный опыт и хотел поделиться.


person ttsiodras    schedule 27.01.2011    source источник
comment
В этом посте есть несколько полезных ответов. stackoverflow .com/questions/150355/   -  person Dan    schedule 27.01.2011
comment
К сожалению, это не очень помогает — все они сообщают число, включающее ядра с гиперпоточностью...   -  person ttsiodras    schedule 27.01.2011
comment
Я обнаружил, что «гиперпоточность» может быть дерьмом для многих приложений. Я отключил его (в биосе) во многих случаях из-за того, что приложения больше не работают или работают намного хуже. Это не просто информация (видно и на мощности).   -  person Marm0t    schedule 27.01.2011


Ответы (3)


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

Одна библиотека, которая поддерживает несколько платформ, называется hwloc и поддерживает Linux и Windows (и другие ), чипы Intel и AMD. Hwloc позволит вам узнать все о топологии оборудования и знает разницу между ядрами и аппаратными потоками (называемые PU - процессорными единицами - в терминологии hwloc). Таким образом, вы должны вызвать эту библиотеку в начале, найти количество фактических ядер и вызвать omp_set_num_threads() (или просто добавить эту переменную в качестве директивы в начале параллельных разделов).

person Jonathan Dursi    schedule 27.01.2011
comment
Спасибо! Это отвечает на мой вопрос. - person ttsiodras; 28.01.2011

К сожалению, ваше предположение о том, почему это происходит, скорее всего, верно. Чтобы быть уверенным, вам пришлось бы использовать инструмент профиля — но я видел это раньше с трассировкой лучей, так что это не удивительно. В любом случае в настоящее время нет возможности определить из OpenMP, что некоторые из процессоров являются «настоящими», а некоторые — гиперпоточными. Вы можете написать некоторый код, чтобы определить это, а затем установить число самостоятельно. Однако все еще остается проблема, заключающаяся в том, что OpenMP не планирует потоки на самих процессорах — он позволяет ОС делать это.

В языковом комитете OpenMP ARB была проведена работа, чтобы попытаться определить стандартный способ для пользователя определить свою среду и сказать, как работать. В настоящее время эта дискуссия все еще продолжается. Многие реализации позволяют вам «привязывать» потоки к процессорам с помощью переменной среды, определенной реализацией. Однако пользователь должен знать нумерацию процессоров и какие процессоры являются «настоящими», а какие процессорами с гиперпоточностью.

person ejd    schedule 27.01.2011
comment
Спасибо - я думаю, мне нужно вернуться к pthread_create, CreateThread и Ко .... И реализовать перенос #pragma parallel для schedule (dynamic, N) самостоятельно? Будет не весело... - person ttsiodras; 27.01.2011

Проблема в том, как OMP использует HT. Это не пропускная способность памяти! Я попробовал простую петлю на моем HT PIV 2,6 ГГц. Результат потрясающий...

С ОМП:

    $ time ./a.out 
    4500000000
    real    0m28.360s
    user    0m52.727s
    sys 0m0.064s

Без OMP: $ time ./a.out 4500000000

    real0   m25.417s
    user    0m25.398s
    sys 0m0.000s

Код:

    #include <stdio.h>
    #define U64 unsigned long long
    int main() {
      U64 i;
      U64 N = 1000000000ULL; 
      U64 k = 0;
      #pragma omp parallel for reduction(+:k)
      for (i = 0; i < N; i++) 
      {
        k += i%10; // last digit
      }
      printf ("%llu\n", k);
      return 0;
    }
person Alex Loktionoff    schedule 04.06.2011