Подтвердить что ты не робот

Hyper-threading... сделал мой рендерер в 10 раз медленнее

Исполнительное резюме: Как можно указать в своем коде, что OpenMP должен использовать только потоки для REAL-ядер, т.е. Не считать гиперпотоки?

Подробный анализ. На протяжении многих лет я в свое свободное время кодировал SW-only, рендеринг с открытым исходным кодом (растеризатор/raytracer). Код GPL и двоичные файлы Windows доступны здесь:   https://www.thanassis.space/renderer.html Он компилируется и работает под Windows, Linux, OS/X и BSD.

В прошлом месяце я представил режим raytracing - и качество сгенерированных снимков было ракетами. К сожалению, raytracing на порядок медленнее, чем растрирование. Чтобы увеличить скорость, так же, как и для растеризаторов, я добавил поддержку OpenMP (и TBB) в raytracer - чтобы легко использовать дополнительные ядра процессора. Растрирование и растрирование легко поддаются потоковой обработке (работа на треугольник - работа на пиксель).

В домашних условиях, с моим Core2Duo, второе ядро ​​помогало всем режимам - как режимы растрирования, так и raytracing получили ускорение, которое находится между 1,85x и 1,9x.

Проблема: Естественно, мне было любопытно видеть верхнюю производительность процессора (я также "играю" с графическими процессорами, предварительный Порт CUDA), поэтому я хотел получить прочную базу для сравнения. Я дал код моему хорошему другу, у которого есть доступ к машине "зверя" с 16-ядерным супер процессором Intel стоимостью 1500 $.

Он запускает его в "самом тяжелом" режиме, режиме raytracer...

... и он получает одну пятую скорость моего Core2Duo (!)

Гасп - ужас. Что сейчас произошло?

Мы начали пробовать разные модификации, патчи... и в итоге мы это поняли.

Используя переменную окружения OMP_NUM_THREADS, можно контролировать количество порожденных потоков OpenMP. По мере увеличения количества потоков от 1 до 8 скорость возрастала (близка к линейному увеличению). В тот момент, когда мы пересекли 8, скорость начала уменьшаться, пока она не опустилась до пятой скорости моего Core2Duo, когда использовались все 16 ядер!

Почему 8?

Потому что 8 было числом реальных ядер. Остальные 8 были... гиперпотоками!

Теория: Теперь это было для меня новостью - я видел, что гиперпоточность помогла много (до 25%) в других алгоритмах, так что это было неожиданно. По-видимому, несмотря на то, что у каждого гиперпотокового ядра есть свои собственные регистры (и SSE-модуль?), Raytracer не мог использовать дополнительную вычислительную мощность. Что заставило меня думать...

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

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

Итак, вопрос: OpenMP обнаруживает количество "ядер" и порождает потоки, чтобы соответствовать им, то есть включает в себя гиперпотоки "ядра" в расчете. В моем случае это, по-видимому, приводит к катастрофическим результатам, по скорости. Кто-нибудь знает, как использовать OpenMP API (если возможно, переносимо) только для создания потоков для REAL-ядер, а не для гиперпотоков?

P.S. Код открыт (GPL) и доступен по ссылке выше, не стесняйтесь воспроизводить на вашей собственной машине - я предполагаю, что это произойдет во всех гиперпотоковых процессорах.

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

4b9b3361

Ответ 1

В принципе, вам нужен довольно переносимый способ запроса среды для довольно низкоуровневых деталей аппаратного обеспечения - и, как правило, вы не можете делать это только из системных вызовов (OS обычно не осознает даже разницу между аппаратными потоками и ядер).

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

Ответ 2

К сожалению, ваше предположение о том, почему это происходит, скорее всего, правильно. Разумеется, вам придется использовать инструмент профиля, но я видел это раньше с помощью raytracing, так что это не удивительно. В любом случае, в настоящее время нет способа определить из OpenMP, что некоторые из процессоров являются "реальными", а некоторые - гиперпотоковыми. Вы можете написать код, чтобы определить это, а затем установить номер самостоятельно. Тем не менее, все равно будет проблема, что OpenMP не планирует потоки на самих процессорах - это позволяет ОС делать это.

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

Ответ 3

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

С OMP:

    $ 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;
    }