将一个小数组排序成一个大排序数组
Sorting a small array into a large sorted array
将大型排序数组与小型未排序数组合并的最佳算法是什么?
我将根据我的特定用例举例说明我的意思,但不要被它们束缚:我主要是想了解问题。
8 MB 排序数组和 92 kB 未排序数组(缓存内排序)
2.5 GB 排序数组和 3.9 MB 未排序数组(内存排序)
34 GB 排序数组和 21 MB 未排序数组(out-of-memory 排序)
你可以实现一个基于块的算法来有效地解决这个问题(无论数组的输入大小是多少,只要一个比另一个小得多)。
首先,您需要对小数组进行排序(可能使用 基数排序 或 双调排序 如果您不这样做需要自定义比较器)。
然后想法是将大数组切成完全适合 CPU 缓存的块(例如 256 KiB)。
对于每个块,使用二进制搜索找到小数组中最后一项 <= 到块的最后一项的索引。
这是相对较快的,因为小数组可能适合缓存,如果数组很大,则二分搜索的相同项目会在连续的块之间获取。
该索引使您能够知道在写入之前有多少项目需要与块合并。
对于块中要合并的每个值,在块中使用二进制搜索找到值的索引。
这很快,因为块适合缓存。
一旦知道要插入到块中的值的索引,就可以在每个块中逐块有效地移动项目(可能从末尾就地移动到开头)。
此实现比 traditional merge algorithm 快得多,因为由于二进制搜索和块插入的项目数量少,所需的比较次数要少得多。
对于比较大的输入,可以使用并行实现。这个想法是同时处理一组多个块(即超级块)。
超级块比经典块大得多(例如 >=2 MiB)。
每个线程一次处理一个超级块。对小数组执行二进制搜索,以了解每个超级块中插入了多少个值。
这个数字在线程之间共享,以便每个线程都知道它可以独立于其他线程安全地写入输出的位置(可以使用并行扫描算法在大规模并行架构上执行此操作)。然后将每个超级块拆分为经典块,并使用先前的算法在每个线程中独立解决问题。
即使在小输入数组不适合缓存的情况下,这种方法也应该更有效,因为整个小数组中的二进制搜索操作的数量将显着减少。
算法的(摊销)时间复杂度为 O(n (1 + log(m) / c) + m (1 + log(c)))
,其中 m
大数组的长度,n
小数组的长度和 c
块大小(为了清楚起见,这里忽略了超级块,但它们只会像常数 c
那样通过常数因子改变复杂性)。
替代方法/优化:如果您的比较运算符很便宜并且可以使用 SIMD 指令进行矢量化,那么您可以优化传统的合并算法。传统方法很慢,因为有分支(一般情况下很难预测),也因为它不能 easily/efficiently 向量化。但是,由于大数组比小数组大很多,传统算法会在小数组之间从大数组中选取很多连续的值。这意味着您可以选择大数组的 SIMD 块并将值与小数组之一进行比较。如果所有 SIMD 项目都小于从小数组中选取的项目,那么您可以非常高效地一次写入整个 SIMD 块。否则,需要先写一部分SIMD chunk,再写small array的item,切换到下一个。最后一个操作显然效率较低,但应该很少发生,因为小数组比大数组小得多。注意小数组还是要先排序
将大型排序数组与小型未排序数组合并的最佳算法是什么?
我将根据我的特定用例举例说明我的意思,但不要被它们束缚:我主要是想了解问题。
8 MB 排序数组和 92 kB 未排序数组(缓存内排序)
2.5 GB 排序数组和 3.9 MB 未排序数组(内存排序)
34 GB 排序数组和 21 MB 未排序数组(out-of-memory 排序)
你可以实现一个基于块的算法来有效地解决这个问题(无论数组的输入大小是多少,只要一个比另一个小得多)。
首先,您需要对小数组进行排序(可能使用 基数排序 或 双调排序 如果您不这样做需要自定义比较器)。 然后想法是将大数组切成完全适合 CPU 缓存的块(例如 256 KiB)。 对于每个块,使用二进制搜索找到小数组中最后一项 <= 到块的最后一项的索引。 这是相对较快的,因为小数组可能适合缓存,如果数组很大,则二分搜索的相同项目会在连续的块之间获取。 该索引使您能够知道在写入之前有多少项目需要与块合并。 对于块中要合并的每个值,在块中使用二进制搜索找到值的索引。 这很快,因为块适合缓存。 一旦知道要插入到块中的值的索引,就可以在每个块中逐块有效地移动项目(可能从末尾就地移动到开头)。 此实现比 traditional merge algorithm 快得多,因为由于二进制搜索和块插入的项目数量少,所需的比较次数要少得多。
对于比较大的输入,可以使用并行实现。这个想法是同时处理一组多个块(即超级块)。 超级块比经典块大得多(例如 >=2 MiB)。 每个线程一次处理一个超级块。对小数组执行二进制搜索,以了解每个超级块中插入了多少个值。 这个数字在线程之间共享,以便每个线程都知道它可以独立于其他线程安全地写入输出的位置(可以使用并行扫描算法在大规模并行架构上执行此操作)。然后将每个超级块拆分为经典块,并使用先前的算法在每个线程中独立解决问题。 即使在小输入数组不适合缓存的情况下,这种方法也应该更有效,因为整个小数组中的二进制搜索操作的数量将显着减少。
算法的(摊销)时间复杂度为 O(n (1 + log(m) / c) + m (1 + log(c)))
,其中 m
大数组的长度,n
小数组的长度和 c
块大小(为了清楚起见,这里忽略了超级块,但它们只会像常数 c
那样通过常数因子改变复杂性)。
替代方法/优化:如果您的比较运算符很便宜并且可以使用 SIMD 指令进行矢量化,那么您可以优化传统的合并算法。传统方法很慢,因为有分支(一般情况下很难预测),也因为它不能 easily/efficiently 向量化。但是,由于大数组比小数组大很多,传统算法会在小数组之间从大数组中选取很多连续的值。这意味着您可以选择大数组的 SIMD 块并将值与小数组之一进行比较。如果所有 SIMD 项目都小于从小数组中选取的项目,那么您可以非常高效地一次写入整个 SIMD 块。否则,需要先写一部分SIMD chunk,再写small array的item,切换到下一个。最后一个操作显然效率较低,但应该很少发生,因为小数组比大数组小得多。注意小数组还是要先排序