你能以多快的速度进行线性搜索?

时间:2010-04-30 01:50:26

标签: c optimization search simd linear

我正在寻求优化这种线性搜索:

static int
linear (const int *arr, int n, int key)
{
        int i = 0;
        while (i < n) {
                if (arr [i] >= key)
                        break;
                ++i;
        }
        return i;
}

对数组进行排序,并且该函数应返回大于或等于键的第一个元素的索引。它们的数组不大(低于200个元素),并且会为大量搜索准备一次。如果需要,可以在第n个之后将数组元素初始化为适当的数组元素,如果这样可以加快搜索速度。

不,不允许二进制搜索,只允许线性搜索。

修改:我现在总结了有关此主题的所有知识in this blog post

20 个答案:

答案 0 :(得分:18)

  1. 告诉你的老板你可以把它提高50%,但需要6个月,还有一些钱。
  2. 等六个月。
  3. 购买新硬件。
  4. 嗯,它与通过排序数组进行线性搜索一样有意义!

    (更严重的是,你能给我们一些关于为什么没有二分搜索的线索吗?)

答案 1 :(得分:17)

到目前为止,您收到了多条建议,其中大多数建议指出线性搜索对排序数据没有意义,而二进制搜索将更有效地工作。这通常恰好是那些不太关心问题的人所做出的那些流行的“听起来正确”的断言之一。实际上,如果考虑到更大的图景,在适当的情况下,线性搜索可以比二分搜索更有效。

请注意,如果我们考虑对排序数组进行搜索查询,则二进制搜索比线性搜索更有效。对此没有任何争论。此外,当您对同一数据执行多个完全随机查询时,二进制搜索仍会胜过线性搜索。

然而,如果我们考虑顺序搜索查询并且这些查询不是完全随机的,则图片开始改变。想象一下,查询按排序顺序到达,即每个下一个查询的值都高于上一个查询。即查询也排序。顺便说一下,它们不必全局且严格地排序,查询序列可能不时地“重置”,即查询低值,但平均来说,后续查询应按递增顺序到达。换句话说,查询到达系列,每个系列按升序排序。在这种情况下,如果系列的平均长度与数组的长度相当,则线性搜索将优于二进制搜索。但是,要利用这种情况,您必须以增量方式实现搜索。很简单:如果下一个查询大于前一个查询,则无需从数组的开头开始搜索。相反,您可以从上一次搜索停止的位置进行搜索。最简单的实现(仅用于说明这个想法)可能如下所示

static int linear(const int *arr, int n, int key)
{
  static int previous_key = INT_MIN;
  static int previous_i = 0;

  i = key >= previous_key ? previous_i : 0;

  while (i < n) {
    if (arr[i] >= key)
      break;
    ++i;
  }

  previous_key = key;
  previous_i = i;

  return i;
}

(免责声明:由于数组从外部作为参数到达的显而易见的原因,上述实现非常难看,而前一个搜索状态是在内部存储的。当然,这是在实践中这样做的错误方法。但是再次,上述内容旨在说明这个想法,而不是更多)。

请注意,无论系列的长度如何,使用上述方法处理每个有序查询序列的复杂性始终为O(N)。使用二进制搜索,复杂度为O(M * log N)。因此,由于显而易见的原因M接近N,即查询以足够长的有序序列到达,上述线性搜索将明显优于二分搜索,而对于小M二元搜索会赢。

此外,即使有序的查询序列不是很长,考虑到您 使用线性搜索,上述修改可能仍会显着提高搜索性能。

P.S。作为有关问题结构的补充信息:

如果您需要在长度为N的有序数组中执行搜索,并且事先知道查询将以[近似,平均]长度M的有序序列到达,则最佳算法将如下所示

  1. 计算步幅S = [N/M]。将S的值“捕捉”到[最近] 2的幂也可能有意义。将排序后的数组想象为长度为S的块序列 - 所谓的 S -blocks
  2. 收到查询后,对可能包含查询值的S块执行增量线性搜索,即它是带有步幅S的普通线性搜索(当然,请记住从上一次搜索停止的块开始。)
  3. 找到S块后,在S块中执行二进制搜索查询值。
  4. 以上是最优化的增量搜索算法,从某种意义上说,它实现了重复搜索的渐近效率的理论极限。请注意,如果M的值远小于N,则算法会“自动”转移到二进制搜索,而当M接近时N算法“自动”支持线性搜索。后者是有道理的,因为在这样的环境中,线性搜索比二分搜索更有效。

    这一切只是为了说明这样一个事实,即“排序数组上的线性搜索总是无用的”这样的说法只表示那些发表此类声明的人缺乏知识。

答案 2 :(得分:12)

由于您可以在最后一个有效条目之后放置已知值,因此添加一个额外的元素n + 1 = max以确保循环不会超过数组的末尾,而不必测试i&lt; Ñ

static int
linear (const int *arr, int n, int key)
{
        assert(arr[n] >= key);
        int i = 0;
        while (arr[i] < key) {
                ++i;
        }
        return i;
}

你也可以尝试展开循环,使用相同的标记值:

static int
linear (const int *arr, int n, int key)
{
        assert(arr[n] >= key);
        int i = 0;
        while (true) {
                if (arr [i++] >= key)
                        break;
                if (arr [i++] >= key)
                        break;
                if (arr [i++] >= key)
                        break;
                if (arr [i++] >= key)
                        break;
        }
        return --i;
}

答案 3 :(得分:7)

首先,任何快速解决方案都必须使用矢量化来同时比较多个元素。

然而,到目前为止发布的所有矢量化实现都存在一个常见问题:它们具有分支。因此,他们必须引入阵列的块处理(以减少分支的开销),这导致小阵列的低性能。对于大型阵列,线性搜索比优化良好的二进制搜索更糟糕,因此优化它是没有意义的。

然而,线性搜索可以在没有分支的情况下实现。这个想法很简单:你想要的索引恰好是数组中少于你搜索的键的元素数。因此,您可以将数组的每个元素与键值进行比较,并将所有标志相加:

static int linear_stgatilov_scalar (const int *arr, int n, int key) {
    int cnt = 0;
    for (int i = 0; i < n; i++)
        cnt += (arr[i] < key);
    return cnt;
}

关于这个解决方案的一个有趣的事情是它会返回相同的答案,即使你洗牌数组=)虽然这个解决方案似乎相当慢,但它可以优雅地矢量化。下面提供的实现要求数组为16字节对齐。此外,必须使用INT_MAX元素填充数组,因为它一次消耗16个元素。

static int linear_stgatilov_vec (const int *arr, int n, int key) {
    assert(size_t(arr) % 16 == 0);
    __m128i vkey = _mm_set1_epi32(key);
    __m128i cnt = _mm_setzero_si128();
    for (int i = 0; i < n; i += 16) {
        __m128i mask0 = _mm_cmplt_epi32(_mm_load_si128((__m128i *)&arr[i+0]), vkey);
        __m128i mask1 = _mm_cmplt_epi32(_mm_load_si128((__m128i *)&arr[i+4]), vkey);
        __m128i mask2 = _mm_cmplt_epi32(_mm_load_si128((__m128i *)&arr[i+8]), vkey);
        __m128i mask3 = _mm_cmplt_epi32(_mm_load_si128((__m128i *)&arr[i+12]), vkey);
        __m128i sum = _mm_add_epi32(_mm_add_epi32(mask0, mask1), _mm_add_epi32(mask2, mask3));
        cnt = _mm_sub_epi32(cnt, sum);
    }
    cnt = _mm_hadd_epi32(cnt, cnt);
    cnt = _mm_hadd_epi32(cnt, cnt);
//  int ans = _mm_extract_epi32(cnt, 0);    //SSE4.1
    int ans = _mm_extract_epi16(cnt, 0);    //correct only for n < 32K
    return ans;
}

单个SSE2寄存器的最终减少只能在必要时使用SSE2来实现,它不应该真正影响整体性能。

我在Intel Core2 Duo E4700上用Visual C ++ 2013 x64编译器进行了测试(很老,是的)。使用rand()提供的元素生成大小为197的数组。所有测试的完整代码是here。这是执行32M搜索的时间:

[OP]
Time = 3.155 (-896368640) //the original OP's code
[Paul R]
Time = 2.933 (-896368640)
[stgatilov]
Time = 1.139 (-896368640) //the code suggested

OP的原始代码每秒处理1060万个阵列(每秒21亿个元素)。建议的代码每秒处理2950万个数组(每秒58亿个元素)。 此外,建议的代码适用于较小的数组:即使对于15个元素的数组,它仍然比OP的原始代码快三倍。

以下是生成的程序集:

$LL56@main:
    movdqa  xmm2, xmm4
    movdqa  xmm0, xmm4
    movdqa  xmm1, xmm4
    lea rcx, QWORD PTR [rcx+64]
    pcmpgtd xmm0, XMMWORD PTR [rcx-80]
    pcmpgtd xmm2, XMMWORD PTR [rcx-96]
    pcmpgtd xmm1, XMMWORD PTR [rcx-48]
    paddd   xmm2, xmm0
    movdqa  xmm0, xmm4
    pcmpgtd xmm0, XMMWORD PTR [rcx-64]
    paddd   xmm1, xmm0
    paddd   xmm2, xmm1
    psubd   xmm3, xmm2
    dec r8
    jne SHORT $LL56@main
$LN54@main:
    phaddd  xmm3, xmm3
    inc rdx
    phaddd  xmm3, xmm3
    pextrw  eax, xmm3, 0

最后,我要指出,只要间隔变小,就可以通过切换到所描述的矢量化线性搜索来更快地优化二进制搜索。

更新:有关此事的更多信息可在my blog post中找到。

答案 4 :(得分:3)

如果目标特定的解决方案是可接受的,那么您可以非常轻松地使用SIMD(SSE,AltiVec或任何您可用的)通过一次测试4个元素而不是仅仅1来获得~4倍的加速。

出于兴趣,我将一个简单的SIMD实现放在一起如下:

int linear_search_ref(const int32_t *A, int32_t key, int n)
{
    int result = -1;
    int i;

    for (i = 0; i < n; ++i)
    {
        if (A[i] >= key)
        {
            result = i;
            break;
        }
    }
    return result;
}

int linear_search(const int32_t *A, int32_t key, int n)
{
#define VEC_INT_ELEMS 4
#define BLOCK_SIZE (VEC_INT_ELEMS * 32)
    const __m128i vkey = _mm_set1_epi32(key);
    int vresult = -1;
    int result = -1;
    int i, j;

    for (i = 0; i <= n - BLOCK_SIZE; i += BLOCK_SIZE)
    {
        __m128i vmask0 = _mm_set1_epi32(-1);
        __m128i vmask1 = _mm_set1_epi32(-1);
        int mask0, mask1;

        for (j = 0; j < BLOCK_SIZE; j += VEC_INT_ELEMS * 2)
        {
            __m128i vA0 = _mm_load_si128(&A[i + j]);
            __m128i vA1 = _mm_load_si128(&A[i + j + VEC_INT_ELEMS]);
            __m128i vcmp0 = _mm_cmpgt_epi32(vkey, vA0);
            __m128i vcmp1 = _mm_cmpgt_epi32(vkey, vA1);
            vmask0 = _mm_and_si128(vmask0, vcmp0);
            vmask1 = _mm_and_si128(vmask1, vcmp1);
        }
        mask0 = _mm_movemask_epi8(vmask0);
        mask1 = _mm_movemask_epi8(vmask1);
        if ((mask0 & mask1) != 0xffff)
        {
            vresult = i;
            break;
        }
    }
    if (vresult > -1)
    {
        result = vresult + linear_search_ref(&A[vresult], key, BLOCK_SIZE);
    }
    else if (i < n)
    {
        result = i + linear_search_ref(&A[i], key, n - i);
    }
    return result;
#undef BLOCK_SIZE
#undef VEC_INT_ELEMS
}

在2.67 GHz Core i7上,使用OpenSUSE x86-64和gcc 4.3.2,我在一个相当广泛的“最佳点”周围进行了7x - 8x改进,其中n = 100000,其中键位于中点数组(即结果= n / 2)。当n变大并且阵列因此超过高速缓存大小(在这种情况下可能变为内存带宽限制)时,性能下降到大约3.5x。当n很小时,性能也会下降,因为SIMD实现效率低(当然,它已针对大型n进行了优化)。

答案 5 :(得分:2)

我知道这个主题已经过时了,但我无法阻止自己发帖。我对哨兵线性搜索的优化是:

int sentinel_linear_search(int key, int *arr, int n)
{
    int last_value, i;

    /* considering that n is the real size of the array */
    if (--n < 1)
        return -1;

    last_value = arr[n];

    /* set array last member as the key */
    arr[n] = key;

    i = 0;
    while (arr[i] != key)
        ++i;

    /* recover the real array last member */
    arr[n] = last_value;

    return (arr[i] == key) ? i : -1;
}

哨兵搜索的重大改进是它的迭代仅使用一个条件分支(键)而不是两个(索引和键)。

    while (arr[i] != key)
        ++i;

答案 6 :(得分:2)

您已收到许多改进建议,但您需要衡量每项优化,以确定哪些最适合您的硬件和编译器

作为一个例子,在这个响应的第一个版本中,我猜测,通过100-200个数组元素,二进制搜索的略高开销应该很容易通过少量探测数据来支付。但是,在下面的评论中,Mark Probst报告说他看到线性搜索在他的硬件上提前约500个条目。这增强了在搜索最佳性能时进行测量的必要性。

注意:根据Mark的评论,编辑了他对线性与二元搜索相比较小N的测量结果。

答案 7 :(得分:2)

你可以并行完成。

如果列表很小,可能不值得拆分搜索,但如果必须处理大量搜索,那么您可以最终并行运行它们。这不会减少操作的延迟,但会提高吞吐量。

答案 8 :(得分:2)

如果您使用的是英特尔平台:

int linear (const int *array, int n, int key)
{
  __asm
  {
    mov edi,array
    mov ecx,n
    mov eax,key
    repne scasd
    mov eax,-1
    jne end
    mov eax,n
    sub eax,ecx
    dec eax
end:
  }
}

但只发现完全匹配,不大于或等于匹配。

在C中,您还可以使用Duff's Device

int linear (const int *array, int n, int key)
{
  const int
    *end = &array [n];

  int
    result = 0;

  switch (n % 8)
  {
    do {
  case 0:
    if (*(array++) >= key) break;
    ++result;
  case 7:
    if (*(array++) >= key) break;
    ++result;
  case 6:
    if (*(array++) >= key) break;
    ++result;
  case 5:
    if (*(array++) >= key) break;
    ++result;
  case 4:
    if (*(array++) >= key) break;
    ++result;
  case 3:
    if (*(array++) >= key) break;
    ++result;
  case 2:
    if (*(array++) >= key) break;
    ++result;
  case 1:
    if (*(array++) >= key) break;
    ++result;
    } while(array < end);
  }

  return result;
}

答案 9 :(得分:2)

如果您有量子计算机,可以使用Grover's algorithm在O(N 1/2 )时间内搜索数据并使用O(log N)存储空间。否则,你的问题很愚蠢。二进制搜索或其变体之一(例如三元搜索)确实是您的最佳选择。当您选择优质算法时,对线性搜索进行微优化是非常愚蠢的。

答案 10 :(得分:1)

展开固定数组索引。

int linear( const int *array, int n, int key ) {
  int i = 0;
  if ( array[n-1] >= key ) {
     do {
       if ( array[0] >= key ) return i+0;
       if ( array[1] >= key ) return i+1;
       if ( array[2] >= key ) return i+2;
       if ( array[3] >= key ) return i+3;
       array += 4;
       i += 4;
     } while ( true );
  }
  return -1;
}

答案 11 :(得分:1)

你可以避免类似于循环展开的n次检查

static int linear(const int *array, int arraySize, int key)
{
  //assuming the actual size of the array is always 1 less than arraySize
  array[arraySize] = key; 

  int i = 0;
  for (; ; ++i)
  {
     if (array[i] == key) return i;
  }
}

答案 12 :(得分:0)

您可以一次搜索比int更大的元素 - 具体来说,平台可以更快或更慢,具体取决于它如何处理更大的数据读取。例如,在64位系统上,一次读取数组2个元素并单独检查高/低元素可以由于较少的I / O而运行得更快。不过,无论如何,这都是O(n)种类。

答案 13 :(得分:0)

这个答案比我的另一个更加模糊,所以我将它单独发布。它依赖于C保证布尔结果false = 0和true = 1的事实。 X86可以生成没有分支的布尔值,所以它可能更快,但我还没有测试过。像这样的微优化将始终高度依赖于您的处理器和编译器。

和以前一样,调用者负责在数组末尾放置一个sentinel值,以确保循环终止。

确定最佳循环展开量需要一些实验。你想找到减少(或消极)回报的点。我打算参加SWAG,这次尝试8次。

static int
linear (const int *arr, int n, int key)
{
        assert(arr[n] >= key);
        int i = 0;
        while (arr[i] < key) {
                i += (arr[i] < key);
                i += (arr[i] < key);
                i += (arr[i] < key);
                i += (arr[i] < key);
                i += (arr[i] < key);
                i += (arr[i] < key);
                i += (arr[i] < key);
                i += (arr[i] < key);
       }
       return i;
}

编辑:正如Mark指出的那样,此函数在前面一行的每一行中引入了依赖关系,这限制了处理器管道并行运行操作的能力。因此,让我们尝试对函数进行一些小修改以删除依赖项。现在该功能确实需要最后8个哨兵元素。

static int 
linear (const int *arr, int n, int key) 
{ 
        assert(arr[n] >= key);
        assert(arr[n+7] >= key);
        int i = 0; 
        while (arr[i] < key) {
                int j = i;
                i += (arr[j] < key); 
                i += (arr[j+1] < key); 
                i += (arr[j+2] < key); 
                i += (arr[j+3] < key); 
                i += (arr[j+4] < key); 
                i += (arr[j+5] < key); 
                i += (arr[j+6] < key); 
                i += (arr[j+7] < key); 
       } 
       return i; 
} 

答案 14 :(得分:0)

在其中一条评论中,您说数组长度为64。

如果您必须线性地执行此操作,您可以执行以下操作:

int i = -1;
do {
  if (arr[0] >= key){i = 0; break;}
  if (arr[1] >= key){i = 1; break;}
  ...
  if (arr[62] >= key){i = 62; break;}
  if (arr[63] >= key){i = 63; break;}
} while(0);

但是,我非常怀疑这是否比这个二进制搜索更快:*

int i = 0;
if (key >= arr[i+32]) i += 32;
if (key >= arr[i+16]) i += 16;
if (key >= arr[i+ 8]) i +=  8;
if (key >= arr[i+ 4]) i +=  4;
if (key >= arr[i+ 2]) i +=  2;
if (key >= arr[i+ 1]) i +=  1;

*感谢Jon Bentley的那一次。

补充:既然你说这个表是为大量搜索准备的,并且你想要快速,你可以在某处分配一些空间并生成机器码,并将值硬连接到其中。它可以是线性搜索或二进制搜索。如果是二进制,机器代码看起来就像编译器会从中生成的那样:

if (key < value32){
  if (key < value16){
    ...
  }
  else {
    ...
  }
}
else {
  if (key < value48){
    ...
  }
  else {
    ...
  }
}

然后,您只需将其复制到可以调用它的位置。

或者你可以打印上面的代码,编译并动态链接到一个DLL,并加载DLL。

答案 15 :(得分:0)

实际上,这个问题的答案完全依赖于您编写代码的平台。例如:

CPU : Memory speed | Example CPU | Type of optimisation
========================================================================
    Equal          |    8086     | (1) Loop unrolling
------------------------------------------------------------------------
  CPU > RAM        |  Pentium    | (2) None
  1. 避免循环访问数据所需的条件分支会略微提高性能。
  2. 一旦CPU开始变得比RAM快,那么循环的效率无关紧要(除非它是一个非常糟糕的循环),由于必须等待从中引入数据,它将停止运行内存。 SIMD并没有真正帮助,因为必须等待更多数据到达才能超越并行测试的优势。当你的CPU受限时,SIMD真的会自成一体。

答案 16 :(得分:0)

这可能会强制执行向量指令(由Gman建议):

for (int i = 0; i < N; i += 4) {
    bool found = false;   
    found |= (array[i+0] >= key);
    ...
    found |= ( array[i+3] >= key);
    // slight variation would be to use max intrinsic
    if (found) return i;
}
...
// quick search among four elements

这也会减少分支指令。 通过确保输入数组与16字节边界对齐来提供帮助

另一件可能有助于矢量化的事情(进行垂直最大比较):

for (int i = 0; i < N; i += 8) {
    bool found = false;   
    found |= max(array[i+0], array[i+4]) >= key;
    ...
    found |= max(array[i+3], array[i+7] >= key;
    if (found) return i;
}
// have to search eight elements

答案 17 :(得分:0)

uint32 LinearFindSse4( uint8* data, size_t data_len, uint8* finddata, size_t finddatalen )
{
    /**
     * the following is based on...
     * #define haszero(v) (((v) - 0x01010101UL) & ~(v) & 0x80808080UL)
     * we split it into 2 sections
     * first section is:
     * (v) - 0x01010101UL)
     *
     * second section is:
     * ~(v) & 0x80808080UL)
     */
    __m128i ones = _mm_set1_epi8( 0x01 );
    __m128i eights = _mm_set1_epi8( 0x80 );
    __m128i find_field = _mm_set1_epi8( finddata[0] );

    uint32 found_at = 0;
    for (int i = 0; i < data_len; i+=16)
    {
#define CHECKTHIS( n ) if (!memcmp(&data[i+n], &finddata[0], sizeof(finddata))) { found_at = i + n; break; }

        __m128i chunk = _mm_stream_load_si128( (__m128i *)&data[i] );
        __m128i xor_result = _mm_xor_si128( chunk, find_field );
        __m128i first_sec = _mm_sub_epi64( xor_result, ones );
        __m128i second_sec = _mm_andnot_si128( xor_result, eights );

        if(!_mm_testz_si128(first_sec, second_sec))
        {
            CHECKTHIS(0);
            CHECKTHIS(1);
            CHECKTHIS(2);
            CHECKTHIS(3);
            CHECKTHIS(4);
            CHECKTHIS(5);
            CHECKTHIS(6);
            CHECKTHIS(7);
            CHECKTHIS(8);
            CHECKTHIS(9);
            CHECKTHIS(10);
            CHECKTHIS(11);
            CHECKTHIS(12);
            CHECKTHIS(13);
            CHECKTHIS(14);
            CHECKTHIS(15);
        }
    }
    return found_at;
}

答案 18 :(得分:0)

向后循环,这可能会被翻译......

// loop backward

for (int i = arraySize - 1; i >=0; --i)

...对此(“可能”更快):

    mov cx, arraySize - 1
detectionHere:
    ...
    loop detectionHere   

除此之外,只有二进制搜索才能使搜索更快

答案 19 :(得分:-4)

好吧,你可以使用指针......

static int linear(const int *array, int arraySize, int key) {
    int i;

    for(i = 0; i < arraySize; ++i) {
        if(*array >= key) {
            return i;
        }

        ++array;
    }

    return arraySize;
}