通过已知索引重新调整数组的缓存友好复制,收集,散布 - 代码日志

通过已知索引重新调整数组的缓存友好复制,收集,散布

假设我们有一个数组,另一个数组有索引。

data = [1, 2, 3, 4, 5, 7]
index = [5, 1, 4, 0, 2, 3]

我们要从索引位置的数据元素创建一个新数组。结果应该是

[4, 2, 5, 7, 3, 1]

朴素算法适用于O(N),但它执行随机存储器访问。

你可以建议具有相同复杂度的CPU缓存友好算法。

PS
在我的某些情况下,数据数组中的所有元素都是整数。

PPS
数组可能包含数百万个元素。

PPPS我可以使用SSE / AVX或任何其他x64特定的优化

将索引和数据合并到单个数组中。然后使用一些缓存友好的排序算法对这些对(按索引)进行排序。然后摆脱索引。 (您可以将归并/删除索引与排序算法的第一个/最后一次进行组合,以便对此进行一些优化)。

对于缓存友好的O(N)排序使用小到足够的小数(在CPU缓存中最多一半的缓存行数)。

这里是基类派生算法的C实现:

void reorder2(const unsigned size)
{
    const unsigned min_bucket = size / kRadix;
    const unsigned large_buckets = size % kRadix;
    g_counters[0] = 0;

    for (unsigned i = 1; i <= large_buckets; ++i)
        g_counters[i] = g_counters[i - 1] + min_bucket + 1;

    for (unsigned i = large_buckets + 1; i < kRadix; ++i)
        g_counters[i] = g_counters[i - 1] + min_bucket;

    for (unsigned i = 0; i < size; ++i)
    {
        const unsigned dst = g_counters[g_index[i] % kRadix]++;
        g_sort[dst].index = g_index[i] / kRadix;
        g_sort[dst].value = g_input[i];
        __builtin_prefetch(&g_sort[dst + 1].value, 1);
    }

    g_counters[0] = 0;

    for (unsigned i = 1; i < (size + kRadix - 1) / kRadix; ++i)
        g_counters[i] = g_counters[i - 1] + kRadix;

    for (unsigned i = 0; i < size; ++i)
    {
        const unsigned dst = g_counters[g_sort[i].index]++;
        g_output[dst] = g_sort[i].value;
        __builtin_prefetch(&g_output[dst + 1], 1);
    }
}

它与基数分类有两个方面:(1)由于所有计数器都是事先知道的,它不进行计数通过; (2)避免使用基数2的幂。

This C++ code was used for benchmarking(如果要在32位系统上运行,稍微减小kMaxSize常量)。

以下是基准测试结果(Haswell CPU与6Mb缓存):

benchmark results

很容易看出,即使是天真的算法,小数组(低于〜20000个元素)也是缓存友好的。另外,您可能会注意到排序方法开始在图的最后一点缓存不友好(在L3缓存中的0.75缓存行附近的大小/基数)。在这些极限排序方法之间比天真的算法更有效。

在理论上(如果我们将这些算法所需的内存带宽与64字节高速缓存行和4字节值进行比较),则排序算法应该快3倍。实际上我们差距很小,约占20%。如果我们对数据阵列使用较小的16位值,这可能会得到改善(在这种情况下,排序算法大约快1.5倍)。

排序方法的另一个问题是当大小/基数接近2的幂时最差的行为。这可能被忽略(因为没有太多的“坏”大小)或通过使该算法稍微更复杂来修复。

如果我们将传球次数增加到3,则所有3次传球主要使用L1缓存,但内存带宽增加了60%。我使用这段代码来获得实验结果:TL; DR.在确定(实验)最佳基数值后,对于大于4 000 000的尺寸(其中2遍算法使用L3缓存一次通过),我得到了更好的结果,但是有些更糟的结果对于较小的阵列(其中2遍算法对于两个通路使用L2缓存)。如预期的那样,16位数据的性能更好。

结论:性能差异远小于算法复杂度的差异,天真的方法几乎总是更好;如果性能非常重要,并且仅使用2或4字节值,则优选排序方法。

http://stackoverflow.com/questions/34693853/cache-friendly-copying-of-an-array-with-readjustment-by-known-index-gather-sca

本站文章除注明转载外,均为本站原创或编译
转载请明显位置注明出处:通过已知索引重新调整数组的缓存友好复制,收集,散布