Подтвердить что ты не робот

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

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

Когда я должен начать использовать parallel.foreach, только повторение 1 000 000 элементов?

4b9b3361

Ответ 1

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

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

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

Ответ 2

Нет, вы не должны этого делать. Важным моментом здесь является не столько количество итераций, сколько работа. Если ваша работа действительно проста, одновременное выполнение 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;
   }
});

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

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

Ответ 3

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

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

См. также Предоставляет ли Parallel.ForEach количество активных потоков? и Параллельно. Для использования одной задачи на итерацию?

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

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

  • Несколько итераций до многих итераций.
  • Каждая итерация быстро проходит на каждую итерацию медленно.

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

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

В тех случаях, когда цикл имеет очень мало, медленные итерации, и вы хотите больше контроля, вы можете использовать Tasks для их обработки в соответствии с:

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 Thread. Это не типично (на самом деле в сценарии веб-сервера это дико нереально). На практике вы не можете видеть параллелизацию с небольшим количеством итераций.

Код проверки:

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(); 
        } 
    } 
} 

результаты на четырехъядерной машине 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-й итерацией немного больше, чем parallelism. 2 предмета короче, поэтому 3 и с 4 или более итерациями - всего около 0,8 секунды.

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

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

Ответ 4

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

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

Здесь запускается только 2 элемента для работы, но каждый из них занимает некоторое время:

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

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

Ответ 5

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

Ответ 6

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

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

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

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

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

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

Ответ 7

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

Ответ 8

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

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));

}

}