65.9K
CodeProject 正在变化。 阅读更多。
Home

search_n 能否更高效?

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (54投票s)

2004年4月12日

CPOL

18分钟阅读

viewsIcon

134755

downloadIcon

492

本文讨论了最流行的 search_n 实现的效率。此外,它还介绍了一种新的针对随机访问迭代器的 search_n 特化,其性能远远优于最常用的实现。

目录

1. 简介

在本文中,我们将首先讨论最流行的 search_n STL 算法实现的效率。之后,我将演示一种新的针对随机访问迭代器的 search_n 特化,其性能远远优于最常用的实现。这实际上是一个大大改进的 search_n 算法,其时间复杂度显著降低。

我的意图是直奔主题,不赘述 C++ 模板或 STL 本身。为了理解本文,您需要对 C++ 模板、标准模板库 (STL) 以及 C++ 语言有充分的理解。最后,我必须承认 search_n 并不是 STL 中最著名的算法,改进它对大多数 C++ 开发人员来说不会产生太大影响。另一方面,将算法提速 2-8 倍有时会很有用...

2. 算法的使用和行为

search_n 算法已经有足够充分的文档。 [2,4,6] 因此,为了描述该算法的使用和行为,我借用了以下三段(2.1、2.2 和 2.3)来自优秀的 SGI STL 文档。 [1,2]

2.1 原型

Search_n 是一个重载名称;实际上有两个 search_n 函数。

template <class ForwardIterator, class Integer, class T>
ForwardIterator search_n(ForwardIterator first, ForwardIterator last,
                         Integer count, const T& value);

template <class ForwardIterator, class Integer, 
          class T, class BinaryPredicate>
ForwardIterator search_n(ForwardIterator first, ForwardIterator last,
                         Integer count, const T& value,
                         BinaryPredicate binary_pred);

2.2 描述

search_n 在范围 [first, last) 中搜索 count 个连续元素组成的子序列,这些元素都等于 value。它返回指向该子序列开头的迭代器,如果不存在这样的子序列,则返回 last。两个版本的 search_n 在如何确定两个元素是否相同方面有所不同:第一个使用 operator==,第二个使用用户提供的函数对象 binary_pred

第一个版本的 search_n 返回范围 [first, last-count) 中第一个迭代器 i,使得对于范围 [i, i+count) 中的每个迭代器 j*j == value。第二个版本返回范围 [first, last-count) 中第一个迭代器 i,使得对于范围 [i, i+count) 中的每个迭代器 jbinary_pred(*j, value)true

2.3 复杂度

线性。 search_n 最多执行 last-first 次比较。(C++ 标准允许复杂度为 O(n(last-first)),但这不必要的宽松。 search_n 没有理由检查任何元素多次。)

3. 常见实现

我所知道的所有 search_n 实现彼此之间都非常相似,并且在所有已知情况下,都使用相同的算法来处理向前迭代器和随机访问迭代器。下面的流程图 1 详细说明了这样一个典型的 search_n 实现。在所有这些 search_n 版本中,我们可以很容易地将 VC++SGI 实现区分为主导版本。我们将在以下小节 [s3.1.][s3.2.] 中分别讨论这两个版本。为简单起见,我们只讨论 search_n 的第一个变体,它使用 operator== 而不是用户提供的函数对象 binary_pred 来测试相等性。 [s2] 除了使用 binary_pred,第二个变体的设计和行为与其对应版本的设计和行为相同。因此,我们讨论的关于第一个变体的一切也适用于第二个变体。

流程图 1

usual search_n

3.1. VC++ 实现

这个 search_n 实现是随流行的 Microsoft Visual C++ 6.x-7.0 编译器附带的 STL 的一部分。此外,它与 Metrowerks CodeWarrior 8.x-9.x 的 STL 提供的实现也非常相似(尽管 Metrowerks 版本在下面的兴趣点 4 中讨论了一些优势)。换句话说,这是最常用的 search_n 实现之一。

源代码

此处不提供 VC++ search_n 实现的源代码,以避免潜在的版权侵犯。然而,任何拥有 Visual C++ 6.x-7.0 编译器的用户都可以很容易地在 algorithm 头文件中找到这段代码。

关注点

  1. 根据 MSDN 网站上的 VC++ search_n 文档 [4],其时间复杂度为:相对于搜索范围大小呈线性关系(另见第 5 节 [s5] 中的性能测试 B1B2C)。
  2. VC++ search_n 首先计算搜索范围 [_First1, _Last1) 的宽度,然后使用该宽度值来形成控制循环的逻辑表达式。当 _FwdIt1 是随机访问迭代器时,宽度计算显然具有常数时间复杂度,否则为线性时间复杂度。换句话说,当使用此 search_n 实现处理列表和其他不提供随机访问迭代器的序列时,我们将遇到显著的开销(另请参见第 5 节 [s5] 中的性能测试 E)。
  3. VC++ search_n 经常多次检查搜索范围中的许多元素。如上所述,search_n 在范围 [_First1, _Last1) 中搜索 _Count 个连续元素组成的子序列,这些元素都等于 _Val。如果 VC++ 版本遇到一个只有 M (M < _Count) 个连续元素等于 _Val 的子序列,它将判断该特定子序列不够长,并将从当前子序列的第二个元素开始继续搜索(而不是紧邻最后一个不匹配元素的元素)。然后它将立即遇到 M-1 个连续元素等于 _Val,这实际上是初始 M 个元素子序列的最后 M-1 个元素。之后,它还将重新访问初始子序列的最后 M-2 个元素,然后再次是最后 M-3 个元素,依此类推。

    也就是说,当遇到 M (M < _Count) 个连续等于 _Val 的元素时,VC++ search_n 最终会进行 M(M+1)/2 次冗余比较!因此,当搜索范围 [_First1, _Last1) 中有许多元素可能等于 _Val 时,VC++ search_n 实现效率较低(另见第 5 节 [s5] 中的性能测试 D)。

  4. Metrowerks 版本的 search_n 与其 VC++ 对应版本非常相似,但它具有显著优势,即从不多次检查其搜索范围中的任何元素。因此,上述兴趣点 2 适用于 Metrowerks 实现,而兴趣点 3 不适用。

3.2. SGI 实现

以下 search_n 实现是优秀的 SGI STL(版本 3.3)的一部分。 [1,2] 此实现主要用于 GCC 编译器的所有现代版本(包括用于 Darwin 和 MinGW 的 GCC)。因此,这也是最流行的 search_n 实现之一。

源代码

// search_n.  Search for __count consecutive copies of __val.
// (Part of the SGI STL v3.3)

template <class _ForwardIter, class _Integer, class _Tp>
_ForwardIter search_n(_ForwardIter __first, _ForwardIter __last,
                          _Integer __count, const _Tp& __val) {
  __STL_REQUIRES(_ForwardIter, _ForwardIterator);
  __STL_REQUIRES(typename iterator_traits<_ForwardIter>::value_type,
                 _EqualityComparable);
  __STL_REQUIRES(_Tp, _EqualityComparable);

  if (__count <= 0)
    return __first;
  else {
    __first = find(__first, __last, __val);
    while (__first != __last) {
      _Integer __n = __count - 1;
      _ForwardIter __i = __first;
      ++__i;
      while (__i != __last && __n != 0 && *__i == __val) {
        ++__i;
        --__n;
      }
      if (__n == 0)
        return __first;
      else
        __first = find(__i, __last, __val);
    }
    return __last;
  }
}

/*
 *
 * Copyright (c) 1994
 * Hewlett-Packard Company
 *
 * Permission to use, copy, modify, distribute and sell this software
 * and its documentation for any purpose is hereby granted without fee,
 * provided that the above copyright notice appear in all copies and
 * that both that copyright notice and this permission notice appear
 * in supporting documentation.  Hewlett-Packard Company makes no
 * representations about the suitability of this software for any
 * purpose.  It is provided "as is" without express or implied warranty.
 *
 *
 * Copyright (c) 1996
 * Silicon Graphics Computer Systems, Inc.
 *
 * Permission to use, copy, modify, distribute and sell this software
 * and its documentation for any purpose is hereby granted without fee,
 * provided that the above copyright notice appear in all copies and
 * that both that copyright notice and this permission notice appear
 * in supporting documentation.  Silicon Graphics makes no
 * representations about the suitability of this software for any
 * purpose.  It is provided "as is" without express or implied warranty.
 */

关注点

  1. 根据 SGI search_n 文档 [2],其时间复杂度为:线性。 search_n 最多执行 last-first 次比较(另见第 5 节 [s5] 中的性能测试 B1B2C)。
  2. SGI search_n 在适当的时候内部使用 find 算法。因此,它可以利用 find 算法的潜在特化,从而显著提高其性能。(例如,如果 _ForwardIter 的类型是 (char *),那么 find 可能会在内部使用 memchr 函数。)这确实是一个非常好的设计选择!
  3. VC++ 实现相比,SGI search_n 内部的迭代器推进受到显著限制。此实现不需要计算搜索范围 [__first, __last) 的宽度,并且更适合与列表和其他不提供随机访问迭代器的序列一起使用(另见第 5 节 [s5] 中的性能测试 E)。
  4. SGI 文档正确地指出,search_n 没有理由多次检查任何元素 [2],但不幸的是,SGIsearch_n 实现并未完全遵守这一理念。再次强调,search_n 在范围 [__first, __last) 中搜索 __count 个连续元素组成的子序列,这些元素都等于 __val。如果 SGI 版本遇到一个只有 M (M < __count) 个连续元素等于 __val 的子序列,它将判断该特定子序列不够长,并将从最后一个不匹配的元素开始继续搜索(而不是紧邻最后一个不匹配元素的元素)。也就是说,上述代码中的 __first = find(__i, __last, __val) 表达式总是检查 *__i 元素,而 *__i 可能是导致不匹配的元素(因此已经检查过)在之前的 while 循环的 *__i == __val 逻辑表达式中。

    换句话说,SGI search_n 每次遇到一个等于 __val 的元素子序列时,都会进行一次冗余比较。实际上,这在大多数实际情况下应该不是一个大问题,但它确实是 SGI search_n 实现中的一个缺陷(另见第 5 节 [s5] 中的性能测试 D)。

4. 随机访问迭代器的增强实现

在本节中,我将介绍一种增强的 search_n 实现,它只能与随机访问迭代器一起使用。此实现的优势在于其主循环的工作方式 [流程图 2:循环 1]。当搜索满足 search_n 条件的 N 个连续元素时,此主循环只检查搜索范围中每 N 个元素中的一个,而 VC++SGI 版本 [s3] 则逐个检查搜索范围元素 [流程图 1:循环 1]。当然,当主循环找到匹配项时,此 search_n 版本必须回溯,因为匹配的元素可能位于满足 search_n 条件的子序列中间。因此,它将首先向后移动 [流程图 2:循环 3],然后再次向前移动 [流程图 2:循环 2],以准确估算特定子序列的起始位置和长度。然而,回溯发生得相当不频繁,而且没有任何元素被多次检查。因此,预计此 search_n 实现将始终比 VC++SGI 版本更快。为方便起见(并为简洁起见),我以后将使用“DPX search_n”来指代此特定算法。

下面将以流程图和源代码两种形式演示 DPX search_n 实现。之后,我们还将讨论与此实现相关的一些兴趣点。

流程图 2

enhanced search_n

源代码

template <class _RandomAccessIter, class _Integer, class _Tp> inline
_RandomAccessIter search_n(_RandomAccessIter __first, 
    _RandomAccessIter __last, _Integer __count, const _Tp& __val)
{
  if (__count <= 0)
    return __first;
  if (__count == 1)
    return std::find(__first, __last, __val);

  typedef std::iterator_traits<_RandomAccessIter>::difference_type iter_diff;

  iter_diff __tailSize = __last - __first;
  const iter_diff __pattSize = __count;

  if (__tailSize >= __pattSize)
  {
    _RandomAccessIter __backTrack;

    iter_diff __remainder, __prevRemainder;
    const iter_diff __skipOffset = __pattSize - 1;
    
    _RandomAccessIter __lookAhead = __first + __skipOffset;

    for ( ; ; __lookAhead += __pattSize ) // the main loop...
    {
      //__lookAhead here is always pointing to the last element of next 
      //  possible match.
      assert( __tailSize >= __pattSize );
      __tailSize -= __pattSize;

      for ( ; ; ) // the skip loop...
      {
        if (*__lookAhead == __val)
          break;
        if (__tailSize < __pattSize)
          return __last;
        
        __lookAhead += __pattSize;
        __tailSize -= __pattSize;
      }
      
      assert( __tailSize == (__last - __lookAhead) - 1 );
      __remainder = __skipOffset;
      
      for ( __backTrack = __lookAhead - 1; *__backTrack == 
          __val; --__backTrack )
      {
        if (--__remainder == 0)
          return (__lookAhead - __skipOffset); //Success
      }

      for ( ; ; )
      {
        if (__remainder > __tailSize)
          return __last; //failure

        __lookAhead += __remainder;
        __tailSize -= __remainder;

        if (*__lookAhead == __val)
        {
          __prevRemainder = __remainder;
          __backTrack = __lookAhead;

          do
          {
            if (--__remainder == 0)
              return (__lookAhead - __skipOffset); //Success

          } while ( *--__backTrack == __val);
          
          //adjust remainder for next comparison
          __remainder += __pattSize - __prevRemainder;
        }
        else
          break;
      }

      //__lookAhead here is always pointing to the element of the last 
      //mismatch.
      if (__tailSize < __pattSize)
        return __last;
    }
  }

  return __last; //failure
}

/*
*  Copyright (c) 2004-2007
*  Jim Xochellis
*
*  Permission to use, copy, modify, distribute and sell this software for 
*  any purpose is hereby granted without fee, provided that the above
*  copyright notice appears in all copies and that both that copyright
*  notice and this permission notice appear in supporting documentation.
*  Jim Xochellis makes no representations about the suitability of this
*  software for any purpose. It is provided "as is" without express or
*  implied warranty.
*
*/

关注点

  1. DPX search_n 最多执行 last-first 次比较,因此其最坏情况复杂度是线性的,类似于 VC++SGI 实现的平均复杂度。但这种最坏情况实际上在该算法中极少发生。另一方面,其平均时间复杂度将为 O((last-first)/count)(另见第 5 节 [s5] 中的性能测试 AB1B2C)。
  2. 除了 DPX search_n 实际上只检查少数满足条件的元素外,我还尽力确保搜索范围中的任何元素都不会被多次检查(另见第 5 节 [s5] 中的性能测试 D)。因此,此 search_n 版本非常适合在元素比较过程较慢的情况下使用。
  3. 基于上述原因,我相信这个 search_n 实现将在所有可能的情况下都优于 VC++SGI 实现。它唯一的缺点是它需要随机访问迭代器,因此不能与只提供前向迭代器的列表和其他序列一起使用(另请参阅第 5 节 [s5] 中的性能测试和第 6 节 [s6] 中的结论)。

5. 性能测试

到目前为止,我们已经遇到了 search_n 算法的三种不同实现,并且我们还在理论上讨论了它们的预期运行时性能。在本节中,我们将观察当我们在计算机上运行这些算法时实际会发生什么。特别是,我使用本文开头提供的代码进行了一系列性能测试。在接下来的小节中,将对每个测试进行描述,并简要讨论其结果。所有性能测试的结果都已在本节末尾以图表形式可视化。

在接下来的段落中,我将频繁使用一些新符号,它们是:

  1. 符号 N,表示 search_n 正在搜索的连续元素数量。
  2. 符号 M,表示 search_n 在搜索过程中超越的序列元素数量。

性能测试 A

此测试的目的是观察 DPX search_n 算法 [s4] 在被超越元素数量 M 增加时的行为。该测试重复了三次,针对 N 的三个不同值。此测试中搜索的元素存储在 vector 中,遇到匹配元素的概率为 1%。

测试 A 的结果已在相应的 图 A 中可视化。图的垂直轴表示经过时间,水平轴表示被超越的元素 M。此图清楚地表明,经过时间随 M 线性增长,但也很明显,随着 N 的增加,DPX search_n 的性能要好得多。这强烈表明特定 search_n 实现的时间复杂度为 O(M/N),而最流行的实现通常具有时间复杂度 O(M)

性能测试 B1 和 B2

这些测试的目的是观察和比较迄今讨论的所有三种 search_n 实现在被超越元素数量 M 增加时的行为。测试 B1测试 B2 之间唯一的区别是 N 的值,它们分别等于 4 和 32。这两个测试中搜索的元素都存储在 vector 中,遇到匹配元素的概率为 1%。

测试 B1B2 的结果已在相应的图 B1B2 中可视化。这两个图的垂直轴都表示经过时间,水平轴都表示被超越的元素 M。两个图都显示,在所有三种 search_n 实现中,经过时间随 M 线性增长。SGI 版本似乎总是比其 VC++ 对应版本表现更好,而 DPX 版本则超越了所有竞争对手。在 测试 B2 中,N 的值为 32 而不是 4,SGIVC++ 版本的性能与其在 测试 B1 中的相应性能相同,而 DPX 版本的性能显著提高。这正是我们所期望的,因为我们知道这三种算法的时间复杂度。(DPX search_n 的时间复杂度为 O(M/N),而另外两个版本的时间复杂度为 O(M))。

性能测试 C

此测试的目的是观察和比较迄今讨论的所有三种 search_n 实现的行为,当被超越元素数量 M 保持不变(一百万个元素),而 N 的值不断增加时。搜索的元素存储在 vector 中,遇到匹配元素的概率为 1%。

测试 C 的结果已在相应的 图 C 中可视化。图的垂直轴表示经过时间,水平轴表示 N。图显示,SGIVC++ 版本的 search_nN 变化时不受影响,而 DPX 版本在 N 增加时表现得更好。这是 DPX search_n 时间复杂度为 O(M/N) 的又一证明。

性能测试 D

此测试的目的是观察和比较迄今讨论的所有三种 search_n 实现的行为,当 MN 保持不变(分别为一百万个元素和 32 个),但匹配元素的密度正在增加时。也就是说,在此测试中,我们增加了遇到匹配元素的概率。搜索的元素存储在 vector 中。

测试 D 的结果已在相应的 图 D 中可视化。图的垂直轴表示经过时间,水平轴表示匹配元素的密度。图显示,当匹配元素的密度变高时,SGIVC++ search_n 版本的性能显著降低,而 DPX 版本几乎不受影响。这是因为当匹配元素的密度很高时,SGIVC++ 版本进行了许多冗余比较(测试 D 中最坏情况:+100%)。在相同情况下,DPX 版本只检查少数(测试 D 中最坏情况:9%)满足条件的元素。

性能测试 E

此测试的目的是观察和比较 SGIVC++ search_n 实现的行为,当被超越元素数量 M 增加且搜索的元素存储在 list 中时。请注意,DPX search_n 不能用于此测试,因为它需要随机访问迭代器,而 list 只提供向前迭代器。N 始终等于 16,遇到匹配元素的概率为 1%。

测试 E 的结果已在相应的 图 E 中可视化。图的垂直轴表示经过时间,水平轴表示被超越的元素 M。与之前的图 B1B2 相比,此图显示 SGI search_n 在使用随机访问迭代器时相对于 VC++ 版本的性能优势,在使用前向迭代器时变得更大。

更多细节

所有图表的垂直轴表示经过时间,您可能会注意到这些轴没有提供关于经过时间精确值的任何信息。这是故意的,因为我们不关心单个经过时间,而是需要关注当问题因素发生变化时时间的增长。此外,实际的经过时间仅在我运行这些测试的计算机上,以及该计算机当时特定的硬件和软件配置下才有意义。

用于进行这些测试的代码包含在本文的顶部。这是一段简短而简单的代码,在源文件中带有注释,我相信它可以轻松用于验证我的测试结果,以及进一步实验 search_n 算法的运行时行为。

性能图 A

性能图 B1

Performance graph A Performance graph B1

性能图 B2

性能图 C

Performance graph B2 Performance graph C

性能图 D

性能图 E

Performance graph D Performance graph E

6. 结论

在本文中,我们讨论了 search_n STL 算法的效率。特别是,我们讨论了非常流行的 VC++SGI 版本 [s3],并且我还介绍了新的 DPX search_n [s4],它是 search_n 算法针对随机访问迭代器的高效特化。此外,我还进行了一系列性能测试 [s5],以展示我们在计算机上运行这些算法时实际会发生什么。所有这些讨论和测试的结论可以总结如下:

  • 当涉及到随机访问迭代器时,DPX search_n 是迄今为止最佳选择。其平均时间复杂度要好得多(请参阅第 4 节 [s4] 中的兴趣点 1),性能测试也证实了这一事实(请参阅第 5 节 [s5] 中的测试 A、B1、B2、C)。
  • 当搜索的连续元素数量变大(参见第 5 节 [s5] 中的测试 A 和 C)或遇到匹配元素的概率增加(参见第 5 节 [s5] 中的测试 D)时,DPX search_n 的性能甚至更好。
  • 对于向前访问迭代器,SGI search_n 的性能远优于 VC++ 版本(请参阅第 5 节 [s5] 中的测试 E)。

7. 关于本文的更多信息

7.1. 致谢

文章的部分文本和代码来源于 SGI/HP 的 STL 实现和文档。

版权所有 © 1996-1999

Silicon Graphics Computer Systems, Inc.

特此授予免费使用、复制、修改、分发和销售本软件及其文档的许可,但前提是所有副本中均应出现上述版权声明,并且支持文档中也应出现该版权声明和本许可声明。Silicon Graphics 不对本软件的适用性作任何声明。本软件按“原样”提供,不附带任何明示或暗示的保证。

版权所有 © 1994

Hewlett-Packard Company

特此授予免费使用、复制、修改、分发和销售本软件及其文档的许可,但前提是所有副本中均应出现上述版权声明,并且支持文档中也应出现该版权声明和本许可声明。Hewlett-Packard Company 不对本软件的适用性作任何声明。本软件按“原样”提供,不附带任何明示或暗示的保证。

7.2. 相关新闻

  • 2005-09-12
    • 针对随机访问迭代器的 search_n 特化已整合到 GCC 包的 libstdc++ 库中。
      变更日志发布变更
  • 2005-02-27

7.3. 修订历史

  • 2007-03-23
    • 针对随机访问迭代器的 search_n 特化中的错误修复和性能改进。
  • 2004-04-19
    • 对象潜在的构造和销毁移至循环外部(由 Simon Hughes 提出)。
  • 2004-04-12
    • 初始发布。

8. 参考文献

  1. SGI 标准模板库.
  2. SGI STL 的 search_n 算法.
  3. VC++ 标准模板库.
  4. VC++ STL 的 search_n 算法.
  5. Rogue Wave 标准模板库.
  6. Rogue Wave STL 的 search 和 search_n 算法.
  7. STLport 标准模板库.
© . All rights reserved.