【问题标题】:Is it possible to parallelize an Iterator that uses Yield?是否可以并行化使用 Yield 的迭代器?
【发布时间】:2014-01-22 14:38:21
【问题描述】:

考虑一下这段代码,它扩展了 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,则没有可挂钩Max() 扩展函数的IEnumerable 接口。

“并行化”这个函数需要什么?我尝试过的Parallel.For 的任何形式似乎都不起作用,因为loadedData 数组未被识别为IEnumerable 类型。 (这可能是因为 Single(,,) 被处理为值类型吗?)

(没有答案必须用VB,C#也可以!)

【问题讨论】:

  • 在我看来你根本不需要这个方法。您可以使用Cast。看来你重新发明了Cast 方法
  • 我可能有,但这不是问题所在。问题是:如何并行化?
  • 所以你想在不使用 Paralell API 的情况下并行化它吗?
  • @RobPerkins,ParallelEnumerable.Cast&lt;TResult&gt; 怎么样?
  • @RobPerkins,不,但您可以使其与 AsParallel() 并行...您的代码将变为 target.AsParallel().Cast&lt;T&gt;()

标签: .net parallel.foreach parallel-extensions


【解决方案1】:

由于您已经拥有IEnumerable&lt;T&gt;,因此您可以在其上使用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 秒
  • 3D 数组,ToEnumerable,PLINQ:7.4 秒
  • 3D 阵列,手动循环,串行:4.0 秒
  • 3D 阵列,手动循环,Parallel.ForEach:1.1 秒
  • 锯齿状阵列,手动循环,串行:2.4 秒
  • 锯齿状阵列,手动循环,PLINQ:0.8 秒

【讨论】:

  • 目前我很纠结于多维数组,但是您的其他方法给了我一些想法,谢谢!
  • Enqueue() 函数的速度有多快,你觉得呢?如果它在最里面的循环中有一个位置并存储了(i,j,k,value) 的 4 个元组,那么我将有办法在其他地方做一些我需要的稀疏数组工作。
  • @RobPerkins 如果它是一个稀疏数组,那么我认为它应该足够快。但你必须自己衡量。
  • svick,到目前为止,我的特定数据集的时间安排非常好。 OrderablePartitioner 正在为每个分区的数组中的两个项目创建一个分区,但这不是我所期望的。除非这对于两个 6 核超线程 CPU 来说是正常的?
  • @RobPerkins 你的意思是Paritioner.Create()?这取决于您的第一个维度有多大。在我的四核上,它似乎总是创建大约 10-15 个分区。但如果你不喜欢这样,你可以使用an overload,它可以让你准确指定分区的大小。
猜你喜欢
  • 2011-01-18
  • 2011-04-06
  • 2017-02-23
  • 1970-01-01
  • 2018-01-29
  • 1970-01-01
  • 2015-04-01
  • 1970-01-01
  • 2021-11-05
相关资源
最近更新 更多