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. Извините за длину сообщения, я думал, что это был образовательный опыт и хотел поделиться.
Ответы
Ответ 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;
}