Разочарование производительности с помощью Parallel.For
Я пытаюсь ускорить расчеты с помощью Parallel.For
. У меня есть процессор Intel Core i7 Q840 с 8 ядрами, но мне удается получить коэффициент производительности 4 по сравнению с последовательным циклом for
. Это так хорошо, как можно получить с помощью Parallel.For
, или можно настроить тон вызова метода для повышения производительности?
Вот мой тестовый код, последовательный:
var loops = 200;
var perloop = 10000000;
var sum = 0.0;
for (var k = 0; k < loops; ++k)
{
var sumk = 0.0;
for (var i = 0; i < perloop; ++i) sumk += (1.0 / i) * i;
sum += sumk;
}
и параллельно:
sum = 0.0;
Parallel.For(0, loops,
k =>
{
var sumk = 0.0;
for (var i = 0; i < perloop; ++i) sumk += (1.0 / i) * i;
sum += sumk;
});
Цикл, который я распараллеливаю, включает вычисление с "глобально" определенной переменной, sum
, но это должно составлять только крошечную крошечную долю общего времени в параллельном цикле.
В выпуске Release build (установлен флаг "оптимизировать код" ) последовательный цикл for
занимает 33,7 с на моем компьютере, тогда как цикл Parallel.For
занимает 8,4 с, а коэффициент производительности - всего 4.0.
В диспетчере задач я вижу, что во время последовательного вычисления загрузка процессора составляет 10-11%, тогда как при параллельном вычислении это составляет всего 70%. Я попытался явно установить
ParallelOptions.MaxDegreesOfParallelism = Environment.ProcessorCount
но безрезультатно. Мне непонятно, почему не все мощности процессора назначаются параллельному вычислению?
![Sequential vs. parallel CPU utilization]()
Я заметил, что аналогичный вопрос был поднят на SO до, с еще более неутешительным результатом. Однако этот вопрос также связан с неполным распараллеливанием в сторонней библиотеке. Моя основная проблема заключается в распараллеливании основных операций в основных библиотеках.
UPDATE
В некоторых комментариях было указано, что процессор, который я использую, имеет только 4 физических ядра, которые видны системе как 8 ядер, если включена гиперпоточность. Ради этого я отключил гиперпоточность и повторный бенчмаркинг.
С отключением hyper-threading мои вычисления теперь быстрее, как параллельный, так и последовательный цикл for
(как я думал). Использование ЦП в цикле for
составляет ок. 45% (!!!) и 100% во время цикла Parallel.For
.
Время вычисления для цикла for
15,6 с (более чем в 2 раза быстрее, чем при включенной гиперпотоке) и 6.2 с для Parallel.For
(на 25% лучше, чем при включенной гиперпотоке). Соотношение производительности с Parallel.For
теперь только 2,5, работающее на 4 реальных ядрах.
Таким образом, коэффициент производительности по-прежнему значительно ниже ожидаемого, несмотря на то, что гиперпоточность отключена. С другой стороны, интригует, что загрузка процессора настолько высока во время цикла for
? Может ли быть какая-то внутренняя распараллеливание в этом цикле?
Ответы
Ответ 1
Использование глобальной переменной может вызвать значительные проблемы с синхронизацией, даже если вы не используете блокировки. Когда вы назначаете значение переменной, каждое ядро должно получить доступ к одному и тому же месту в системной памяти или дожидаться завершения работы другого ядра до его доступа.
Вы можете избежать коррупции без блокировок, используя более легкий метод Interlocked.Add, чтобы добавить значение к сумме атомарно на уровне ОС, но вы все равно получите задержки из-за утверждение.
Правильный способ сделать это - обновить локальную переменную потока, чтобы создать частичные суммы и добавить все их в одну глобальную сумму в конце. Parallel.For имеет перегрузку, которая делает именно это. MSDN даже имеет пример с использованием суммы в How To: Write Parallel.For Loop, у которого есть переменные типа Thread
int[] nums = Enumerable.Range(0, 1000000).ToArray();
long total = 0;
// Use type parameter to make subtotal a long, not an int
Parallel.For<long>(0, nums.Length, () => 0, (j, loop, subtotal) =>
{
subtotal += nums[j];
return subtotal;
},
(x) => Interlocked.Add(ref total, x)
);
Каждый поток обновляет свое собственное промежуточное значение и обновляет глобальную общую сумму, используя Interlocked.Add, когда она заканчивается.
Ответ 2
Parallel.For и Parallel.ForEach будут использовать степень parallelism, которая, по его мнению, уместна, балансируя затраты на настройку и срыв потоков и работу, которую он ожидает, каждый поток будет выполнять. .NET 4.5 сделала несколько улучшений производительности (включая более интеллектуальные решения по количеству потоков для разворота) по сравнению с предыдущими версиями .NET.
Обратите внимание, что даже если бы он включал один поток на ядро, переключатели контекста, ложный обмен, блокировки ресурсов и другие проблемы может препятствовать достижению линейной масштабируемости (в общем, не обязательно с вашим примером кода).
Ответ 3
Я думаю, что коэффициент вычислений настолько низок, потому что ваш код "слишком прост" для работы над другой задачей на каждой итерации - потому что parallel.for просто создает новую задачу на каждой итерации, так что это займет время, чтобы обслуживать их в потоках. Я буду так:
int[] nums = Enumerable.Range(0, 1000000).ToArray();
long total = 0;
Parallel.ForEach(
Partitioner.Create(0, nums.Length),
() => 0,
(part, loopState, partSum) =>
{
for (int i = part.Item1; i < part.Item2; i++)
{
partSum += nums[i];
}
return partSum;
},
(partSum) =>
{
Interlocked.Add(ref total, partSum);
}
);
Partitioner создаст оптимальную часть задания для каждой задачи, будет меньше времени для задачи обслуживания с потоками. Если вы можете, пожалуйста, сравните это решение и сообщите нам, если он будет лучше ускоряться.
Ответ 4
foreach vs parallel для каждого примера
for (int i = 0; i < 10; i++)
{
int[] array = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29 };
Stopwatch watch = new Stopwatch();
watch.Start();
//Parallel foreach
Parallel.ForEach(array, line =>
{
for (int x = 0; x < 1000000; x++)
{
}
});
watch.Stop();
Console.WriteLine("Parallel.ForEach {0}", watch.Elapsed.Milliseconds);
watch = new Stopwatch();
//foreach
watch.Start();
foreach (int item in array)
{
for (int z = 0; z < 10000000; z++)
{
}
}
watch.Stop();
Console.WriteLine("ForEach {0}", watch.Elapsed.Milliseconds);
Console.WriteLine("####");
}
Console.ReadKey();
![введите описание изображения здесь]()
Мой процессор
Процессор Intel® Core ™ i7-620M (кэш 4M, 2,66 ГГц)