这是微软放弃的我们框架用户的另一个版本。它的速度是Panos Theof 的解决方案和Eric J和Petar Petrov 的并行Array.Clear
解决方案的 4 倍和更快- 对于大型阵列来说,速度高达两倍。
首先,我想向您展示函数的祖先,因为这样更容易理解代码。在性能方面,这与 Panos Theof 的代码几乎相当,并且对于某些可能已经足够的事情:
public static void Fill<T> (T[] array, int count, T value, int threshold = 32)
{
if (threshold <= 0)
throw new ArgumentException("threshold");
int current_size = 0, keep_looping_up_to = Math.Min(count, threshold);
while (current_size < keep_looping_up_to)
array[current_size++] = value;
for (int at_least_half = (count + 1) >> 1; current_size < at_least_half; current_size <<= 1)
Array.Copy(array, 0, array, current_size, current_size);
Array.Copy(array, 0, array, current_size, count - current_size);
}
如您所见,这是基于已初始化部分的重复加倍。这是简单而高效的,但它与现代内存架构相冲突。因此诞生了一个版本,它只使用加倍来创建一个缓存友好的种子块,然后在目标区域上迭代地爆破:
const int ARRAY_COPY_THRESHOLD = 32; // 16 ... 64 work equally well for all tested constellations
const int L1_CACHE_SIZE = 1 << 15;
public static void Fill<T> (T[] array, int count, T value, int element_size)
{
int current_size = 0, keep_looping_up_to = Math.Min(count, ARRAY_COPY_THRESHOLD);
while (current_size < keep_looping_up_to)
array[current_size++] = value;
int block_size = L1_CACHE_SIZE / element_size / 2;
int keep_doubling_up_to = Math.Min(block_size, count >> 1);
for ( ; current_size < keep_doubling_up_to; current_size <<= 1)
Array.Copy(array, 0, array, current_size, current_size);
for (int enough = count - block_size; current_size < enough; current_size += block_size)
Array.Copy(array, 0, array, current_size, block_size);
Array.Copy(array, 0, array, current_size, count - current_size);
}
注意:前面的代码需要(count + 1) >> 1
作为加倍循环的限制,以确保最终的复制操作有足够的素材来覆盖剩下的所有内容。如果count >> 1
改为使用奇数计数,则不会出现这种情况。对于当前版本,这无关紧要,因为线性复制循环会弥补任何不足。
数组单元的大小必须作为参数传递,因为 - 令人难以置信 - 不允许使用泛型,sizeof
除非它们使用unmanaged
将来可能会或可能不会变得可用的约束 ( )。错误的估计没什么大不了的,但如果值准确,性能最好,原因如下:
Array.Clear
这是我的代码和前面提到的其他三个解决方案的基准测试。时间用于填充Int32[]
给定大小的整数数组 ( )。为了减少由缓存变幻莫测等引起的变化,每个测试都执行了两次,背靠背,并为第二次执行计时。
array size Array.Clear Eric J. Panos Theof Petar Petrov Darth Gizka
-------------------------------------------------------------------------------
1000: 0,7 µs 0,2 µs 0,2 µs 6,8 µs 0,2 µs
10000: 8,0 µs 1,4 µs 1,2 µs 7,8 µs 0,9 µs
100000: 72,4 µs 12,4 µs 8,2 µs 33,6 µs 7,5 µs
1000000: 652,9 µs 135,8 µs 101,6 µs 197,7 µs 71,6 µs
10000000: 7182,6 µs 4174,9 µs 5193,3 µs 3691,5 µs 1658,1 µs
100000000: 67142,3 µs 44853,3 µs 51372,5 µs 35195,5 µs 16585,1 µs
如果这段代码的性能不够,那么一个有前途的途径是并行化线性复制循环(所有线程使用相同的源块),或者我们的好老朋友 P/Invoke。
注意:块的清除和填充通常由运行时例程完成,这些运行时例程使用 MMX/SSE 指令和诸如此类的分支到高度专业化的代码,因此在任何体面的环境中,人们只需调用各自的道德等价物std::memset
并确保专业性能水平。IOW,按照权利,库函数Array.Clear
应该让我们所有的手卷版本都尘埃落定。事实恰恰相反,这表明事情真的很不正常。首先必须推出自己Fill<>
的产品也是如此,因为它仍然只在核心和标准中,而不是在框架中。.NET 已经存在将近 20 年了,我们仍然需要左右 P/Invoke 来获取最基本的东西,或者推出我们自己的...