Должен ли я всегда использовать Parallel.Foreach, потому что больше потоков ДОЛЖНО ускорять все? - PullRequest
39 голосов
/ 13 ноября 2010

Имеет ли смысл использовать для каждого нормального foreach цикл параллельного.foreach?

Когда я должен начать использовать параллельный.foreach, только итерируя 1000000 элементов?

Ответы [ 8 ]

66 голосов
/ 13 ноября 2010

Нет, это не имеет смысла для каждого foreach.Некоторые причины:

  • Ваш код не может на самом деле быть распараллеливаемым.Например, если вы используете «результаты до сих пор» для следующей итерации и порядок важен)
  • Если вы агрегируете (например, суммируете значения), то есть способы использования Parallel.ForEach дляэто, но вы не должны делать это вслепую
  • Если ваша работа все равно будет очень быстрой, это не принесет никакой пользы, и это может сильно замедлить процесс

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

19 голосов
/ 13 ноября 2010

Нет, вам определенно не следует этого делать. Важным моментом здесь является не количество итераций, а работа, которую необходимо выполнить. Если ваша работа действительно проста, параллельное выполнение 1000000 делегатов добавит огромных накладных расходов и, скорее всего, будет медленнее, чем традиционное однопоточное решение. Вы можете обойти это, разделив данные, поэтому вместо этого вы выполняете куски работы.

например. рассмотрим ситуацию ниже:

Input = Enumerable.Range(1, Count).ToArray();
Result = new double[Count];

Parallel.ForEach(Input, (value, loopState, index) => { Result[index] = value*Math.PI; });

Операция здесь настолько проста, что накладные расходы, связанные с параллельным выполнением, уменьшат выгоду от использования нескольких ядер. Этот код работает значительно медленнее, чем обычный цикл foreach.

Используя раздел, мы можем уменьшить накладные расходы и реально наблюдать увеличение производительности.

Parallel.ForEach(Partitioner.Create(0, Input.Length), range => {
   for (var index = range.Item1; index < range.Item2; index++) {
      Result[index] = Input[index]*Math.PI;
   }
});

Мораль этой истории такова, что параллелизм сложен, и вы должны применять его только после тщательного изучения ситуации. Кроме того, вы должны профилировать код до и после добавления параллелизма.

Помните, что независимо от какого-либо потенциального выигрыша в производительности параллелизм всегда добавляет сложности к коду, поэтому, если производительность уже достаточно высока, нет причин добавлять сложность.

13 голосов
/ 29 августа 2012

Краткий ответ: нет , вы не должны просто использовать Parallel.ForEach или связанные конструкции в каждом цикле, который вы можете.Параллель имеет некоторые издержки, что не оправдано в циклах с небольшим количеством быстрых итераций.Кроме того, break значительно сложнее внутри этих циклов.

Parallel.ForEach - это запрос на планирование цикла, когда планировщик задач сочтет нужным, основываясь на количестве итераций в цикле, количестве ядер ЦП наоборудование и текущая нагрузка на это оборудование.Фактическое параллельное выполнение не всегда гарантируется и менее вероятно, если имеется меньше ядер, число итераций мало и / или текущая нагрузка высока.

См. Также Ограничивает ли Parallel.ForEach ограничениеколичество активных потоков? и Использует ли Parallel.For одну задачу на итерацию?

Длинный ответ:

Мы можем классифицироватьцикл по тому, как они падают на две оси:

  1. От нескольких итераций до многих итераций.
  2. Каждая итерация проходит быстро, и каждая итерация медленная.

Третий фактор - это если задачи сильно различаются по длительности - например, если вы вычисляете точки на множестве Мандельбротанекоторые точки быстро рассчитываются, некоторые занимают гораздо больше времени.

Когда существует несколько быстрых итераций, вероятно, не стоит использовать распараллеливание каким-либо образом, скорее всего, оно будет медленнее из-за накладных расходов.Даже если распараллеливание ускоряет конкретный небольшой быстрый цикл, оно вряд ли будет представлять интерес: выигрыш будет небольшим, и это не является узким местом для производительности в вашем приложении, поэтому оптимизируйте его на удобочитаемость, а не производительность.

Где циклимеет очень мало медленных итераций, и вам нужен больший контроль, вы можете рассмотреть возможность использования Задач для их обработки по следующим направлениям:

var tasks = new List<Task>(actions.Length); 
foreach(var action in actions) 
{ 
    tasks.Add(Task.Factory.StartNew(action)); 
} 
Task.WaitAll(tasks.ToArray());

Там, где много итераций, Parallel.ForEach находится в его элементе.

Документация Microsoft гласит, что

При выполнении параллельного цикла TPL разделяет источник данных, чтобы цикл мог работать одновременно на нескольких частях.За кулисами планировщик задач разделяет задачу на основе системных ресурсов и рабочей нагрузки.Когда это возможно, планировщик перераспределяет работу между несколькими потоками и процессорами, если рабочая нагрузка становится неуравновешенной.

Это разделение и динамическое перепланирование будет труднее сделать эффективно, так как число итераций цикла уменьшается,и является более необходимым, если итерации различаются по продолжительности и при наличии других задач, выполняющихся на одном и том же компьютере.

Я запустил некоторый код.

Результаты теста нижепокажите машину, на которой больше ничего не запущено, и никаких других потоков из пула потоков .Net.Это не типично (на самом деле в сценарии с веб-сервером это крайне нереально).На практике вы можете не увидеть распараллеливания с небольшим числом итераций.

Тестовый код:

namespace ParallelTests 
{ 
    class Program 
    { 
        private static int Fibonacci(int x) 
        { 
            if (x <= 1) 
            { 
                return 1; 
            } 
            return Fibonacci(x - 1) + Fibonacci(x - 2); 
        } 

        private static void DummyWork() 
        { 
            var result = Fibonacci(10); 
            // inspect the result so it is no optimised away. 
            // We know that the exception is never thrown. The compiler does not. 
            if (result > 300) 
            { 
                throw new Exception("failed to to it"); 
            } 
        } 

        private const int TotalWorkItems = 2000000; 

        private static void SerialWork(int outerWorkItems) 
        { 
            int innerLoopLimit = TotalWorkItems / outerWorkItems; 
            for (int index1 = 0; index1 < outerWorkItems; index1++) 
            { 
                InnerLoop(innerLoopLimit); 
            } 
        } 

        private static void InnerLoop(int innerLoopLimit) 
        { 
            for (int index2 = 0; index2 < innerLoopLimit; index2++) 
            { 
                DummyWork(); 
            } 
        } 

        private static void ParallelWork(int outerWorkItems) 
        { 
            int innerLoopLimit = TotalWorkItems / outerWorkItems; 
            var outerRange = Enumerable.Range(0, outerWorkItems); 
            Parallel.ForEach(outerRange, index1 => 
            { 
                InnerLoop(innerLoopLimit); 
            }); 
        } 

        private static void TimeOperation(string desc, Action operation) 
        { 
            Stopwatch timer = new Stopwatch(); 
            timer.Start(); 
            operation(); 
            timer.Stop(); 

            string message = string.Format("{0} took {1:mm}:{1:ss}.{1:ff}", desc, timer.Elapsed); 
            Console.WriteLine(message); 
        } 

        static void Main(string[] args) 
        { 
            TimeOperation("serial work: 1", () => Program.SerialWork(1)); 
            TimeOperation("serial work: 2", () => Program.SerialWork(2)); 
            TimeOperation("serial work: 3", () => Program.SerialWork(3)); 
            TimeOperation("serial work: 4", () => Program.SerialWork(4)); 
            TimeOperation("serial work: 8", () => Program.SerialWork(8)); 
            TimeOperation("serial work: 16", () => Program.SerialWork(16)); 
            TimeOperation("serial work: 32", () => Program.SerialWork(32)); 
            TimeOperation("serial work: 1k", () => Program.SerialWork(1000)); 
            TimeOperation("serial work: 10k", () => Program.SerialWork(10000)); 
            TimeOperation("serial work: 100k", () => Program.SerialWork(100000)); 

            TimeOperation("parallel work: 1", () => Program.ParallelWork(1)); 
            TimeOperation("parallel work: 2", () => Program.ParallelWork(2)); 
            TimeOperation("parallel work: 3", () => Program.ParallelWork(3)); 
            TimeOperation("parallel work: 4", () => Program.ParallelWork(4)); 
            TimeOperation("parallel work: 8", () => Program.ParallelWork(8)); 
            TimeOperation("parallel work: 16", () => Program.ParallelWork(16)); 
            TimeOperation("parallel work: 32", () => Program.ParallelWork(32)); 
            TimeOperation("parallel work: 64", () => Program.ParallelWork(64)); 
            TimeOperation("parallel work: 1k", () => Program.ParallelWork(1000)); 
            TimeOperation("parallel work: 10k", () => Program.ParallelWork(10000)); 
            TimeOperation("parallel work: 100k", () => Program.ParallelWork(100000)); 

            Console.WriteLine("done"); 
            Console.ReadLine(); 
        } 
    } 
} 

результаты на 4-ядерном компьютере с Windows 7:

serial work: 1 took 00:02.31 
serial work: 2 took 00:02.27 
serial work: 3 took 00:02.28 
serial work: 4 took 00:02.28 
serial work: 8 took 00:02.28 
serial work: 16 took 00:02.27 
serial work: 32 took 00:02.27 
serial work: 1k took 00:02.27 
serial work: 10k took 00:02.28 
serial work: 100k took 00:02.28 

parallel work: 1 took 00:02.33 
parallel work: 2 took 00:01.14 
parallel work: 3 took 00:00.96 
parallel work: 4 took 00:00.78 
parallel work: 8 took 00:00.84 
parallel work: 16 took 00:00.86 
parallel work: 32 took 00:00.82 
parallel work: 64 took 00:00.80 
parallel work: 1k took 00:00.77 
parallel work: 10k took 00:00.78 
parallel work: 100k took 00:00.77 
done

Выполнение кода, скомпилированного в .Net 4 и .Net 4.5, дает практически одинаковые результаты.

Последовательные работы выполняются одинаково.Неважно, как вы нарезаете его, оно выполняется примерно за 2,28 секунды.

Параллельная работа с 1 итерацией немного дольше, чем никакой параллелизм.2 элемента короче, так что 3 и с 4 или более итерациями все около 0,8 секунд.

Используются все ядра, но не с эффективностью 100%.Если бы последовательная работа была разделена на 4 части без дополнительных затрат, она бы завершилась за 0,57 секунды (2,28 / 4 = 0,57).

В других сценариях я вообще не видел ускорения с параллельными 2-3 итерациями.У вас нет точного контроля над этим с Parallel.ForEach, и алгоритм может решить разделить их на 1 блок и запустить на 1 ядре, если машина занята.

9 голосов
/ 13 ноября 2010

Нет нижнего предела для выполнения параллельных операций. Если у вас есть только 2 элемента для работы, но каждый из них займет некоторое время, возможно, все же имеет смысл использовать Parallel.ForEach. С другой стороны, если у вас есть 1000000 элементов, но они делают не очень много, параллельный цикл может работать не быстрее обычного цикла.

Например, я написал простую программу для временных вложенных циклов, где внешний цикл выполнялся как с циклом for, так и с Parallel.ForEach. Я рассчитал это на своем 4-х процессорном (двухъядерном, гиперпоточном) ноутбуке.

Вот прогон с двумя предметами для работы, но каждый занимает некоторое время:

2 outer iterations, 100000000 inner iterations:
for loop: 00:00:00.1460441
ForEach : 00:00:00.0842240

Вот пример с миллионами предметов для работы, но они не очень много делают:

100000000 outer iterations, 2 inner iterations:
for loop: 00:00:00.0866330
ForEach : 00:00:02.1303315

Единственный реальный способ узнать это попробовать.

1 голос
/ 13 ноября 2010

Как правило, если вы превысите поток на ядро, каждый дополнительный поток, участвующий в операции, сделает его медленнее, а не быстрее.

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

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

Тем не менее, в наши дни одноядерные машины встречаются все реже, так что может показаться, что вы сможетесделать все как минимум вдвое быстрее с параллельной обработкой.

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

Кроме того, если ваш код изначально многопоточный, чтобы начатьс, вы можете оказаться в ситуации, когда вы по существу конкурируете за ресурсы с самим собой (классическим случаем является код ASP.NET, обрабатывающий одновременные запросы).Здесь преимущество в параллельной работе может означать, что одна тестовая операция на 4-ядерном компьютере приближается к 4-кратной производительности, но как только число запросов, для которых требуется выполнить ту же задачу, достигает 4, тогда каждый из этих 4-х запросов - каждыйпытаясь использовать каждое ядро, становится немного лучше, чем если бы у них было ядро ​​каждое (возможно, немного лучше, возможно, немного хуже).Преимущества параллельной работы, следовательно, исчезают по мере того, как использование переходит от теста с одним запросом к реальному множеству запросов.

1 голос
/ 13 ноября 2010

Не следует слепо заменять каждый цикл foreach в вашем приложении на параллельный foreach.Больше потоков не обязательно означает, что ваше приложение будет работать быстрее.Вам нужно разделить задачу на более мелкие задачи, которые могут выполняться параллельно, если вы действительно хотите использовать несколько потоков.Если ваш алгоритм не распараллелен, вы не получите никакой выгоды.

0 голосов
/ 17 ноября 2014

Это мои тесты, показывающие, что чистый последовательный интерфейс самый медленный, а также различные уровни разбиения.

class Program
{
    static void Main(string[] args)
    {
        NativeDllCalls(true, 1, 400000000, 0);  // Seconds:     0.67 |)   595,203,995.01 ops
        NativeDllCalls(true, 1, 400000000, 3);  // Seconds:     0.91 |)   439,052,826.95 ops
        NativeDllCalls(true, 1, 400000000, 4);  // Seconds:     0.80 |)   501,224,491.43 ops
        NativeDllCalls(true, 1, 400000000, 8);  // Seconds:     0.63 |)   635,893,653.15 ops
        NativeDllCalls(true, 4, 100000000, 0);  // Seconds:     0.35 |) 1,149,359,562.48 ops
        NativeDllCalls(true, 400, 1000000, 0);  // Seconds:     0.24 |) 1,673,544,236.17 ops
        NativeDllCalls(true, 10000, 40000, 0);  // Seconds:     0.22 |) 1,826,379,772.84 ops
        NativeDllCalls(true, 40000, 10000, 0);  // Seconds:     0.21 |) 1,869,052,325.05 ops
        NativeDllCalls(true, 1000000, 400, 0);  // Seconds:     0.24 |) 1,652,797,628.57 ops
        NativeDllCalls(true, 100000000, 4, 0);  // Seconds:     0.31 |) 1,294,424,654.13 ops
        NativeDllCalls(true, 400000000, 0, 0);  // Seconds:     1.10 |)   364,277,890.12 ops
    }


static void NativeDllCalls(bool useStatic, int nonParallelIterations, int parallelIterations = 0, int maxParallelism = 0)
{
    if (useStatic) {
        Iterate<string, object>(
            (msg, cntxt) => { 
                ServiceContracts.ForNativeCall.SomeStaticCall(msg); 
            }
            , "test", null, nonParallelIterations,parallelIterations, maxParallelism );
    }
    else {
        var instance = new ServiceContracts.ForNativeCall();
        Iterate(
            (msg, cntxt) => {
                cntxt.SomeCall(msg);
            }
            , "test", instance, nonParallelIterations, parallelIterations, maxParallelism);
    }
}

static void Iterate<T, C>(Action<T, C> action, T testMessage, C context, int nonParallelIterations, int parallelIterations=0, int maxParallelism= 0)
{
    var start = DateTime.UtcNow;            
    if(nonParallelIterations == 0)
        nonParallelIterations = 1; // normalize values

    if(parallelIterations == 0)
        parallelIterations = 1; 

    if (parallelIterations > 1) {                    
        ParallelOptions options;
        if (maxParallelism == 0) // default max parallelism
            options = new ParallelOptions();
        else
            options = new ParallelOptions { MaxDegreeOfParallelism = maxParallelism };

        if (nonParallelIterations > 1) {
            Parallel.For(0, parallelIterations, options
            , (j) => {
                for (int i = 0; i < nonParallelIterations; ++i) {
                    action(testMessage, context);
                }
            });
        }
        else { // no nonParallel iterations
            Parallel.For(0, parallelIterations, options
            , (j) => {                        
                action(testMessage, context);
            });
        }
    }
    else {
        for (int i = 0; i < nonParallelIterations; ++i) {
            action(testMessage, context);
        }
    }

    var end = DateTime.UtcNow;

    Console.WriteLine("\tSeconds: {0,8:0.00} |) {1,16:0,000.00} ops",
        (end - start).TotalSeconds, (Math.Max(parallelIterations, 1) * nonParallelIterations / (end - start).TotalSeconds));

}

}
0 голосов
/ 13 ноября 2010

Нет. Вы должны понимать, что делает код и поддается ли он распараллеливанию. Зависимости между вашими элементами данных могут затруднить распараллеливание, то есть, если поток использует значение, вычисленное для предыдущего элемента, он должен ждать, пока значение не будет вычислено в любом случае, и не может работать параллельно. Вы также должны понимать свою целевую архитектуру, хотя, как правило, у вас будет многоядерный процессор практически на всем, что вы покупаете в наши дни. Даже на одном ядре вы можете получить некоторые преимущества от большего количества потоков, но только если у вас есть некоторые задачи блокировки. Также следует помнить, что при создании и организации параллельных потоков возникают дополнительные затраты. Если эти накладные расходы составляют значительную долю (или более) времени, которое занимает ваша задача, вы можете замедлить ее.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...