Вложенные пары Parallel.ForEach в том же списке?

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

foreach (var element1 in list)
    foreach (var element2 in list)
        foo(element1, element2);

В этом случае foo не изменит состояние элемента1 или element2. Я знаю, что небезопасно просто выполнять вложенные инструкции Parallel.ForEach:

Parallel.ForEach(list, delegate(A element1)
{
    Parallel.ForEach(list, delegate(A element2)
    {
        foo(element1, element2);
    });
});

Каким будет идеальный способ реализовать это с помощью библиотеки параллельных задач?

Ответ 1

Разве у вас не было бы одного параллельного и одного нормального цикла? Поэтому либо

Parallel.ForEach(list, delegate(A element1)
{
  foreach(A element2 in list)
    foo(element1, element2)
});

или

foreach(A element1 in list)
{
  Parallel.ForEach(list, delegate(A element2)
  {
    foo(element1, element2);
  });
}

Следует также ускорить его. В любом случае никогда не будет нитки за такт, так что это, вероятно, будет так же быстро или немного медленнее, чем вложенные параллельные петли.

Ответ 2

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

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

alt text http://www.freeimagehosting.net/uploads/ca97f403f8.png

На каждой итерации через несколько секунд будет потеряно несколько миллисекунд, чтобы определить, какой поток должен выполнить следующую итерацию. Скажем, у вас есть набор из 7 предметов. Если вы распараллеливаете родительский цикл, эти миллисекунды будут потеряны 7 раз. Если вы распараллеливаете обе петли, они будут потеряны 7 × 7 = 49 раз. Чем больше набор, тем больше перегрев.

Ответ 3

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

EDIT. Вместо создания списка всех комбинаций вы можете использовать итератор для возврата двухэлементного кортежа с комбинацией. Parallel.ForEach все равно будет распараллелить обработку кортежей.

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

 const int SIZE = 10;
    static void Main(string[] args)
    {
        List<int> list = new List<int>(SIZE);
        for(int i=0;i<SIZE;i++)
        {
            list.Add(i);
        }


        Parallel.ForEach(GetCombinations(list),(t,state,l)=>
            Console.WriteLine("{0},{1},{2}",l,t.Item1,t.Item2));

    }

    static IEnumerable<Tuple<int,int>> GetCombinations(List<int> list)
    {
        for(int i=0;i<list.Count;i++)
            for(int j=0;j<list.Count;j++)
                yield return Tuple.Create(list[i],list[j]);
    }