Я не уверен, почему вы определяете производительность как общее количество операций на общее время ЦП , а затем удивляетесь уменьшающейся функцией количества потоков. Это будет почти всегда и всегда, за исключением случаев, когда срабатывают эффекты кеширования. Истинный показатель производительности c - это количество операций на время настенных часов .
Это легко показать с помощью простых математических рассуждений. Учитывая общую работу W
и вычислительную мощность каждого ядра P
, время на одном ядре составляет T_1 = W / P
. Равномерное разделение работы между n
ядрами означает, что каждое из них работает для T_1,n = (W / n + H) / P
, где H
- накладные расходы на поток, вызванные самим распараллеливанием. Их сумма составляет T_n = n * T_1,n = W / P + n (H / P) = T_1 + n (H / P)
. Накладные расходы всегда имеют положительное значение, даже в тривиальном случае так называемого смущающего параллелизма , когда два потока не нуждаются в взаимодействии или синхронизации. Например, запуск потоков OpenMP требует времени. Вы не можете избавиться от накладных расходов, вы можете только амортизировать их в течение срока службы потоков, убедившись, что каждый из них получает много работы. Следовательно, T_n
> T_1
и при фиксированном количестве операций в обоих случаях производительность на n
ядрах всегда будет ниже, чем на одном ядре. Единственным исключением из этого правила является случай, когда данные для работы размером W
не помещаются в кеши нижнего уровня, а данные для работы размера W / n
подходят. Это приводит к значительному ускорению, превышающему количество ядер, известному как сверхлинейное ускорение . Вы измеряете внутри функции потока, поэтому вы игнорируете значение H
, а T_n
должно более или менее быть равным T_1
в пределах точности таймера, но ...
При выполнении нескольких потоков несколько ядер ЦП, все они конкурируют за ограниченные общие ресурсы ЦП, а именно за кеш последнего уровня (если есть), пропускную способность памяти и тепловой пакет.
Пропускная способность памяти не является проблемой, когда вы просто увеличиваете скаляр переменная, но становится узким местом, когда код начинает фактически перемещать данные в ЦП и из него. Каноническим примером численных вычислений является умножение разреженной матрицы на вектор (spMVM) - правильно оптимизированная процедура spMVM, работающая с double
ненулевыми значениями и long
индексами, потребляющая так много полосы пропускания памяти, что можно полностью заполнить память шина всего с двумя потоками на сокет процессора, что делает дорогой 64-ядерный процессор очень плохим выбором в этом случае. Это верно для всех алгоритмов с низкой арифметикой c интенсивностью (операций на единицу объема данных).
Когда дело доходит до тепловой оболочки, большинство современных процессоров используют динамику c управление питанием и будет разгонять или уменьшать частоту ядер в зависимости от того, сколько из них активно. Следовательно, в то время как n
ядер с пониженной тактовой частотой выполняют больше работы за единицу времени , чем одно ядро, одно ядро превосходит n
ядер с точки зрения работы на общее время ЦП , который является метрикой c, которую вы используете.
Имея в виду все это, нужно учитывать еще одну (но не менее важную) вещь - разрешение таймера и шум измерения. Ваше время работы выражается в парах микросекунд. Если ваш код не работает на каком-то специализированном оборудовании, которое ничего не делает, кроме запуска вашего кода (то есть без разделения времени с демонами, потоками ядра и другими процессами и без обработки прерываний), вам нужны тесты, которые работают на несколько порядков дольше, желательно хотя бы на пару секунд.