提高“洗牌”效率

时间:2011-09-30 02:06:01

标签: c# extension-methods

现在,我正在使用以下代码创建一个Shuffle扩展名:

public static class SiteItemExtensions
{
    public static void Shuffle<T>(this IList<T> list)
    {
        var rng = new Random();
        int n = list.Count;
        while (n > 1)
        {
            n--;
            int k = rng.Next(n + 1);
            T value = list[k];
            list[k] = list[n];
            list[n] = value;
        }
    }
}

我正在寻找一种更快捷有效的方法来实现这一目标。现在,使用秒表类,大约需要20秒来洗牌100,000,000件物品。有没有人有任何想法让这更快?

2 个答案:

答案 0 :(得分:4)

这凸显了现代计算机设计的一个方面,这个方面经常被忽视。通过愚蠢的改变,它可以快3倍以上:

            int k = 0; rng.Next(n + 1);  // silly change

现在内循环中有更多语句,但速度要快得多。您看到的是CPU缓存的影响。该算法具有非常差的缓存局部性,从阵列中读取的下一个元素已经在缓存中的可能性非常低。这需要昂贵的旅行到较慢的外部缓存和死慢的内存总线。之后所需的数组元素加载到缓存中的几率非常高。但是它们在需要时仍然存在的可能性非常低,你的列表太大而不适合缓存。

没有什么可以解决这个问题,它是算法设计中固有的。然而,使用实际列表大小是一个明显的解决方在具有1000个元素的列表上运行100,000次是3倍。

答案 1 :(得分:3)

您已超出CPU缓存的功能,并且大部分时间都在等待RAM。

通过逐步降低元素数量,我得到以下结果(在List<int>上):

count      time (s)     slowdown
100000000  16.0429005   11.99215276436421
10000000   01.3377832   20.37312930505406
1000000    00.0656641   13.36837069158574
100000     00.0049119

注意10 ^ 6到10 ^ 7之间的大幅减速。我将元素数量增加了10倍,但时间增加了20倍。这可能是我的CPU不再能够将大部分数组放入第二个(以及我的CPU中的最后一个)缓存级别的地方。 p> 顺便说一句,通过在方法签名中将IList<T>替换为List<T>并避免[]上的interface call penalty,您可以获得一两秒(但失去一般性):

IList<T>:   16.0429005 s
List<T>:    14.3529349 s

对于记录,在Visual C ++ 2010下,std::random_shuffle上的std::vector<int>包含100000000个元素......

17.947 s

...所以你可能已经尽可能快了。

(注意:C#和C ++基准测试都是在调试器之外的各个Release配置下完成的。)