使用List <t> .Sort和IEnumerable </t>进行算法加速

时间:2012-11-30 22:45:32

标签: c# performance algorithm ienumerable icomparable

对于我的项目,我首先从文件加载图像,然后将每个像素放入2D pixels[,]数组中。然后,我想检查每个像素,并根据它们的着色方式将它们分成“箱”,然后对每个箱进行排序。所以我有一个Bin对象,它封装了一个List<Pixel>,我有一个List<Bin>包含一个(相对较小)数量的二进制文件。

我的问题是,当我尝试从非常大的图像(例如1920x1200 = 230万像素)填充这些垃圾箱时,我使用的算法比我想要的慢,并且我已经将问题追溯到某些C#语言特有的功能似乎比我预期的要长。我想就如何更好地使用C#来消除这些瓶颈提出一些建议。

加载图像后,我调用一个名为“fillBinsFromSource”的函数,该函数获取可枚举的像素列表,找到它们所属的bin,并将它们放在那里:

public void fillBinsFromSource(IEnumerable<Pixel> source)
    {
        Stopwatch s = new Stopwatch();
        foreach (Pixel p in source)
        {
            s.Start();
            // algorithm removed for brevity
            // involves a binary search of all Bins and a List.Add call
            s.Stop();
        }           
    }

对于大型图像,预计我的算法需要一段时间,但是当我在函数调用之外放置一个秒表时,事实证明它需要的时间大约是s产生的时间的两倍,意味着使用foreach进行枚举占用此函数的一半时间(对于1920x1200图像,大约800毫秒的1.6秒)。

我需要传递可枚举列表的原因是因为有时用户只会添加图片的一小部分区域,而不是整个图片。耗时的调用会传递几个迭代器,首先是从图像列表,然后是列表中的每个图像,如下所示(简化):

class ImageList : IEnumerable<Pixel>
{
    private List<Image> imageList;
    public IEnumerator<Pixel> GetEnumerator()
    {
        foreach (Image i in imageList)
            foreach (Pixel p in i)
                yield return p;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    } 

class Image : IEnumerable<Pixel>
{
    private Pixel[,] pixels; // all pixels in the image        
    private List<Pixel> selectedPixels;// all pixels in the user's selection

    public IEnumerator<Pixel> GetEnumerator()
    {
        if (selectedPixels == null)
            for (int i = 0; i < image.Width; i++)
                for (int j = 0; j < image.Height; j++)
                    yield return pixels[i, j];
        else
            for (int i = 0; i < selectedPixels.Count; i++)
                yield return selectedPixels[i];
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

然后我最后称之为

ImageList list; // pretend it contains only 1 image, and it's large
fillBinsFromSource(list);

问题1)由于需要枚举像素的2D数组和所选区域,因此根据用户选择的内容,枚举非常慢。我怎样才能加快速度呢?

然后,在用Pixel个对象填充所有这些箱子后,我对它们进行排序。我致电List<Pixel>.Sort()并依赖IComparable,就像这样:

ImageList list; // pretend it contains only 1 image, and it's large
fillBinsFromSource(list);
foreach(Bin b in allBins)
    b.Sort(); // calls List<Pixel>.Sort()


class Pixel : IComparable
{
    // store both HSV and RGB
    float h, s, v;
    byte r, g, b;

    // we sort by HSV's value property
    public int CompareTo(object obj)
    {
        // this is much faster than calling CompareTo on a float
        Pixel rhs = obj as Pixel;
        if (v < rhs.v)
            return -1;
        else if (v > rhs.v)
            return 1;
        return 0;
    }

问题2)假设allBins有7个要素;排序7个单独的列表,其中总共有230万Pixel s,大约需要2秒。对230万随机int的一个列表进行排序需要不到200毫秒。我可以理解使用原始类型有一定程度的加速,但使用IComparable的速度是否真的慢了10倍?这里有加速吗?

我为长期问题道歉,如果有人对我有任何建议,我会很感激!

3 个答案:

答案 0 :(得分:3)

您确实需要对代码进行分析,看看速度有多慢。

最明显的是:

  • Pixel没有实现通用IComparable<Pixel>,因此比较必须做更多的工作。
  • 尝试制作Pixel值类型。很可能你会看到性能下降,但可能不会。
  • 如果您发现性能低于您认为可接受的性能,请考虑直接通过索引而不是迭代器传递2d范围(矩形)和访问Pixel对象。迭代器很好,但不是免费的。

答案 1 :(得分:2)

如果你想要原始金属性能,各种间接,如访客模式或虚拟继承,都是有毒的。虚拟调用,分配,不可预测的分支会对这种算法造成很大的破坏,因为这种算法存在一个小而紧凑的内部循环,其中99,99%的时间都花在了内部。

为什么呢?因为CPU喜欢并行执行许多(几十个)指令。只有当它可以在指令流中向前看时,它才能做到这一点。上述事情阻止了这一点。

你真的需要让最里面的循环正确。不要在那里分配,不要调用虚函数(或接口方法或委托)。

可能,您的最内层循环应该使用硬编码内核处理给定图像的矩形。不是按像素实现处理功能,而是按矩形实现。

相比之下,提供图像流并不重要。在那里使用LINQ你想要的一切。这是一个低容量操作,因为每个图像有数百万像素。

答案 2 :(得分:1)

您可以使用访问者模式,而不是使用迭代器,甚至构建一个数组/像素列表。图像,图像列表和表示任意选择的其他对象都可以使用单个方法VisitPixel接受访问者类,并为对象表示的每个像素调用该方法。然后访问者类负责在访问时对所有像素进行分类。

这可能无需将所有像素提取到单独的数组中。它也可能会消除迭代器的创建,转而使用简单的for循环。

阿列克谢·列文科夫对你的第二个问题有一些好处。使用Sort实例的IComparer<T> overload可能会更快。