在一个非常小的数组中找到最小值

Finding minimum in a very small array

本文关键字:数组 最小值 非常 一个      更新时间:2023-10-16

我正在处理长int数据,我试图确定数组中最小的元素。我知道传统的遍历数组查找最小值的方法。这个问题是为了检查是否有其他方法来加速它。

这个数组的一些属性可能会帮助我们加快速度,但我不确定如何。

数组恰好有8个长整型整数。每次我们调用这个函数,我们从数组中找到一个最小值,然后用另一个数字替换这个数字,然后重复这个步骤。(至少80亿次)

我正在考虑为下一次迭代记住第二大数(因为我们将在当前迭代中比较它们)。与遍历数组的线性实现相比,这有用吗?

也允许排序,但我们必须以某种方式使用临时数组记住原始位置。这样会更有效吗?

也有可能使用SIMD来确定长整数上的最小值吗?即使是一毫秒的加速也是有用的,因为我要做数十亿次这个操作。

对于一个8元素数组的算法的理论复杂度是无关紧要的。考虑到缓存的局部性和其他因素,线性搜索很可能是最好的选择。

另一种方法是将数组按降序排序一次,然后每次简单地替换第一个元素,最终将新数字移到右边。

在任何情况下,尝试和配置文件。

使用SIMD可以做到这一点,因为您可以并行处理多达4个比较。常规的遍历数组的算法不能矢量化,因为每次比较都依赖于前一次比较的结果,例如

x = min(array[0], array[1])
x = min(x, array[2])
x = min(x, array[3))
...

如果您将此更改为一种淘汰赛方法,如果将值0-3加载到一个向量中,将值4-7加载到另一个向量中,则可以一次进行多次比较:

// these 4 ops can be done at once using SIMD
x[0] = min(array[0], array[4])
x[1] = min(array[1], array[5])
x[2] = min(array[2], array[6])
x[3] = min(array[3], array[7])
// so can these 2 ops:
y[0] = min(x[0], x[2])
y[1] = min(x[1], x[3])
z[0] = min(y[0], y[1])

这意味着理论上只需要进行3次向量化比较。

例如,在ARM NEON SIMD中,它看起来像这样(比较8个32位值):
vldm     r1!, {d0-d3}
vmin.32  q0, q0, q1    // first vectorized comparison
vpmin.32 d0, d0, d1    // second comparison
vpmin.32 d0, d0, d1    // third comparison
// min value is now in d0[0]

在最后一个比较中,你最终会做一些不必要的比较,因为它是矢量化的,但这无关紧要。

我使用ARM NEON作为示例,因为我不太熟悉x86 SIMD,但相同的方法应该可以工作,并且可以扩展到64位值,如在这个相关问题

一如既往,确保您的配置文件,不要过早地优化,yadda yadda yadda

您可以以最小堆的形式组织数组。搜索将是O(1),替换将是O(logn)。这将改善从O(n)O(logn)的时间复杂度,这应该是显著的。

因为它只是八个整数,所以按如下步骤进行:

  1. 第一次对8个数字进行排序,保持其原始索引
  2. 以"展开的方式"实现二进制搜索:在你的代码中有8行if/else
  3. 使用二进制查找查找最大的数
  4. 使用二进制搜索找到插入新整数的正确位置

尝试使用min-heap。例如

#include <iostream>
#include <algorithm>
#include <array>
using namespace std;
int main() {
    array<int, 8> arr { 3, 1, 4, 6, 5, 9, 2, 7 };
    make_heap(arr.begin(), arr.end(), greater<int>());
    pop_heap(arr.begin(), arr.end());
    cout << "Min Element: " << arr.back() << endl;
    return 0;
}

1

naïve这里的方式是

*min_element(arr.begin(), arr.end());

或者你可以用multiset

std::multiset<long int> ms { 3, 1, 4, 6, 5, 8, 2, 7 };
for every new_element
    ms.erase(ms.begin());    // ms.begin() is the iterator to min element
    ms.insert(new_element);

我会保留一些信息并更新它。

您有八个值x0到x7。

保持值a0 = max (x0, x1), a2 = max (x2, x3), a4 = max (x4, x5), a6 = max (x6, x7),并记住每对中哪个是最大的。

保持值b0 = max (a0, a2), b4 = max (a4, a6),并记住哪个是每个集合中最大的。

得到最大的元素很简单。当您拥有它并插入一个新元素时,您需要恰好更新值a0、a2、a4和a6中的一个,以及恰好更新值b0和b4中的一个。

(刚刚注意到您正在寻找最小值-应该没有太大区别)。

考虑到N是如此之小,并且替换过程本身是连续的,因此很难在此操作上获得显著的加速。虽然理论上最小堆是一个完美的工具,但我不认为它会带来太多开销。

我的建议是保持数组的递增顺序,并在替换最小值时使用InsertionSort的插入步骤,即将元素一个接一个地移到前面,直到找到插入槽。您可以完全展开代码,以避免检查数组结束条件。

保持元素排序的好处是,一旦找到插入点,就可以停止搜索。平均而言,您可以期望在比较次数方面有所改进(但内存移动次数有所增加):-()

你也可以用二分搜索来找到插入点,进行3到4次比较,但我怀疑它是否能明显胜过线性搜索。


如果您的值适合16位无符号整数,您将非常满意_mm_minpos_epu16指令。


在完全偏执的版本中,您可以通过对将原始数组转换为排序序列的排列进行编号来避免不必要的内存移动。总共有40320个(!)。安排一个巨大的硬编码开关语句,在该语句中,给定手头的排列顺序,按照相关顺序执行线性搜索;然后替换最大值并更新排列索引