Разочарование производительности с помощью 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 ГГц)