是否可以并行化使用Yield的迭代器?

时间:2014-01-22 14:38:22

标签: .net parallel.foreach parallel-extensions

考虑这段代码,它扩展了Array .NET类型:

Public Module ArrayExtensions
    <System.Runtime.CompilerServices.Extension>
    Public Iterator Function ToEnumerable(Of T)(target As Array) As IEnumerable(Of T)
        For Each item In target
            Yield DirectCast(item, T)
        Next
    End Function
End Module

我用它来获得Min()和Max()扩展方法将采用的结构。阵列通常在三维中运行到数百万个元素,例如数组T(,,)很常见。

编辑:具体来说,这个函数与一行代码一起发挥作用:

    Return loadedData(rType).dataArray.ToEnumerable(Of Single).Min

其中dataarray(在这种情况下)是ConcurrentDictionary loadedData中的值项,其类型为Single(,,)

如果没有当前编写的ToEnumerable,则没有IEnumerable接口可以挂接Max()扩展函数。

“并行化”此功能需要什么?我尝试过的Parallel.For形式似乎无效,因为loadedData数组未被识别为IEnumerable类型。 (这是因为单个(,,)被处理为值类型,也许?)

(没有答案必须使用VB.C#也没关系!)

1 个答案:

答案 0 :(得分:1)

由于您已经拥有IEnumerable<T>,因此您可以使用AsParallel()(例如dataArray.ToEnumerable().AsParallel().Min())。但IEnumerable接口本质上是串行的,您可以并行化处理其元素,但不能迭代它。这意味着对于像Min()这样非常简单的操作,这种并行化没有多大意义。

这里有意义的是将迭代并行化。这是可能的,因为您可以使用索引器访问数组的特定项目。

我尝试使用custom partitioner执行此操作,但结果比串行版本更差。问题是每次迭代的开销必须尽可能小,并且很难直接使用分区器。

相反,您可以做的是仅对数组的第一个维度进行分区(假设您可以确定它至少与您的CPU数量一样大),然后使用仅返回ToEnumerable()的版本第一个维度的一部分。类似的东西:

private static IEnumerable<T> ToEnumerable<T>(this T[,,] array, int from, int to)
{
    for (int i = from; i < to; i++)
    {
        for (int j = 0; j < array.GetLength(1); j++)
        {
            for (int k = 0; k < array.GetLength(2); k++)
            {
                yield return array[i, j, k];
            }
        }
    }
}

Partitioner.Create(0, data.GetLength(0))
           .AsParallel()
           .Select(range => data.ToEnumerable(range.Item1, range.Item2).Min())
           .Min()

这大约是我计算机上串行版本的两倍。但是这仍然有枚举器的开销,这在这种情况下非常重要:这个版本大约是上面的并行代码的两倍:

var length0 = data.GetLength(0);
var length1 = data.GetLength(1);
var length2 = data.GetLength(2);

float min = float.MaxValue;

for (int i = 0; i < length0; i++)
{
    for (int j = 0; j < length1; j++)
    {
        for (int k = 0; k < length2; k++)
        {
            float value = data[i, j, k];
            if (value < min)
                min = value;
        }
    }
}

return min;

现在我们可以并行化这段代码,这会导致大约四倍的加速(如前所述,我们对第一个维度进行分区,然后按顺序继续):

var results = new ConcurrentQueue<float>();
var length1 = data.GetLength(1);
var length2 = data.GetLength(2);

Parallel.ForEach(
    Partitioner.Create(0, data.GetLength(0)), range =>
    {
        float min = float.MaxValue;

        for (int i = range.Item1; i < range.Item2; i++)
        {
            for (int j = 0; j < length1; j++)
            {
                for (int k = 0; k < length2; k++)
                {
                    float value = data[i, j, k];
                    if (value < min)
                        min = value;
                }
            }
        }

        results.Enqueue(min);
    });

return results.Min();

但是等等!还有更多。多维数组在.Net中非常慢,因此从性能角度来看,使用锯齿状数组(float[][][]而不是float[,,])是有意义的,即使多维数组更适合。使用它,我们可以获得大约50%的加速:

dataJagged.AsParallel().Min(
    level1 =>
    {
        float min = float.MaxValue;

        foreach (var level2 in level1)
        {
            for (int k = 0; k < level2.Length; k++)
            {
                float value = level2[k];
                if (value < min)
                    min = value;
            }
        }

        return min;
    });

总而言之,我的计算机上有一个使用各种方法的时间表:

  • 3D阵列,ToEnumerable,序列号:18.6 s
  • 3D数组,ToEnumerable,PLINQ:7.4 s
  • 3D阵列,手动循环,串行:4.0秒
  • 3D阵列,手动循环,Parallel.ForEach:1.1 s
  • 锯齿状阵列,手动循环,序列号:2.4 s
  • 锯齿状阵列,手动循环,PLINQ:0.8秒