Hyper-threading ... сделал мой рендерер в 10 раз медленнее - PullRequest
17 голосов
/ 27 января 2011

Резюме : Как можно указать в своем коде, что OpenMP должен использовать потоки только для РЕАЛЬНЫХ ядер, то есть не считать гипер-многопоточность?

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

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

Дома, с моим Core2Duo, второе ядро ​​помогло всем режимам - и режимы растеризации и трассировки лучей получили ускорение от 1,85 до 1,9х.

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

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

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

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

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

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

Почему 8?

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

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

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

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

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

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

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

Ответы [ 3 ]

6 голосов
/ 28 января 2011

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

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

3 голосов
/ 27 января 2011

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

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

1 голос
/ 04 июня 2011

Проблема в том, как 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;
    }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...