现在,我正在使用以下代码创建一个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件物品。有没有人有任何想法让这更快?
答案 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配置下完成的。)