【问题标题】:Why find method for string is faster than my single pass?为什么查找字符串的方法比我的单遍更快?
【发布时间】:2013-03-01 06:55:07
【问题描述】:

我有点震惊,为什么 C++ STL 字符串查找子字符串的 find 方法比简单的 O(n) 传递字符串要快。这里有两个不同的功能: 为什么在str2 中找到str1 的第二个函数比第一个函数快(优化得很好)? 我知道第一个函数的任务略有不同,但它仍然只是通过str1str2 (O(n)),而第二个函数可能需要O(n^2) 才能在str2 中找到str1。 真的为什么?你们有什么想法吗?提前谢谢你。

P.S 这些功能是更大项目的一部分。它们在我的代码中被调用了很多次来比较两个字符串。如果我使用第二个函数,整个代码的运行时间几乎是一半(135 秒 VS 235 秒)!

bool Is_Included1(string str1, string str2)
{
    size_t i,s;
    s=str1.size();
    if (s<=str2.size())
    {
        for (i=0;i<s;i++)
            if (str1[i]!=str2[i])
                return false;
        return true;
    }
    return false;
}


bool Is_Included2(string str1, string str2)
{
    size_t i;
    if (str1.size()<=str2.size())
    {
        i=str2.find(str1);
        if (i==0)
            return true;
        else
            return false;
    }
    return false;
}

【问题讨论】:

  • “我知道第一个函数的任务稍有不同” ok....
  • 第一个函数做了一个非常不同的事情。这在任何方面都不适合“轻微”......第一个只是检查第一个字符串是否是第二个字符串的前缀......
  • 我知道它不一样,但是为什么当我使用第一个时,它会变慢。这是我的问题。
  • 我在测试后确认 STL 是否比手工制作的快,不知何故,大量使用的库比自制代码快是更正常的。答案肯定在 STL 代码中......
  • fr.wikipedia.org/wiki/… (fr) Boyer Moore 算法的例子是 O(n+m) 而不是 O(n.m) ...

标签: c++ performance stl


【解决方案1】:

我已经跟踪了 GCC 4.7.2 中的实现。 它的复杂度是O(nm),其中n,m是两个字符串的长度。

假设n.size()小于m.size(),对于m的每一个可能的起点i,它首先比较n[0]和m[i](traits_type::eq),然后调用traits_type: :compare,它实际上执行 __builtin_memcmp()。

这不是确切的实现,但它说明了算法。

for (size_t i=0; i<m.size(); ++i) {
    if (traits_type::eq(n[0], m[i]) &&
        traits_type::compare(n[1], m[i+1], n.size()-1) == 0) {
            return i;
    }
}
return -1;

虽然算法的时间顺序更差,但我猜是因为 __builtin_memcmp() 没有逐一比较字符,因此变得比我们预期的要快。

顺便说一句,如果你经常调用函数,你应该传递两个字符串的 const 引用而不是值传递,这会导致不必要的复制。

如下:

bool Is_Included2(const string& str1, const string& str2)
{
    if (str1.size() > str2.size()) return false;
    return str2.find(str1) == 0;
}

【讨论】:

    【解决方案2】:

    原因必须至少部分是您查询的特定结构,找出答案是一个有趣的侦探挑战!例如,当 str2 比 str1 长得多(并且不包含完全不同的字符)时,您的实现显然会更快。为避免混淆,我们现在假设两个字符串的长度相同。

    可能的解释是您的 STL 版本实现使用 CPU 上可用的较长寄存器对字符进行批量比较。您可以将多个字符打包到一个寄存器中,然后并行比较它们。这样,您可以一步比较几个连续的字符(即使使用标准的 64 位寄存器,您也可以打包 8 个字符并同时比较它们)。请参阅This stack overflow question 进行讨论。

    另一种可能的解释是,STL 使用一种算法,例如,从字符串的结尾开始比较字符串,如果字符串的差异往往大于字符串的前缀,则从结尾开始比较。

    您可以通过运行测试来检查:速度差异是由于匹配还是不匹配,还是两者兼而有之?对于我的第二个解释,您会看到 STL 版本中的不匹配更好,第一个解释会加快匹配速度。

    【讨论】:

      【解决方案3】:

      区别在于数组访问器[i] 与指针算法。

      使用str1[i]str2[i] 是主要区别。这些访问器通常不会像使用底层指针算法那样优化,例如。 const char* c1 = str1.cstr(),然后执行 ++c1; ++c2 遍历它们(这是任何 STL 实现在底层所做的)。

      一般来说,底层硬件在迭代指针而不是数组方面做得更好。有时编译器可以优化循环以使用指针算术而不是数组算术,但由于std::string 使用operator[] 的复杂重载实现,它基本上总是在循环的每次迭代中最终执行arrayBase+offset

      试试这个:

      bool Is_Included1(string str1, string str2)
      {
          size_t i,s;
          s=str1.size();
          if (s<=str2.size())
          {
              const char* c1 = str1.c_str();
              const char* c2 = str2.c_str();
              for (i=0;i<s;i++, c1++, c2++)
                  if (*c1!=*c2)
                      return false;
              return true;
          }
          return false;
      }
      

      看看它与 STL 参考实现的比较。

      (请注意,STL 版本可能仍然快一点,因为现在您可以进一步优化它以完全删除 int i 的使用)

      【讨论】:

      • 我要添加一个一般建议:在 C++ 中,忘记曾经存在的数组访问!迭代器更高效、更灵活,因为它们也适用于其他没有随机访问的容器。也主要适用于 C。
      • 好吧,如果编译器可以内联operator[],它应该也可以优化它。但这有两个先决条件:启用内联并关闭断言(检查,检查,检查......是的,operator[] 确实包含断言)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-07-16
      • 2015-03-14
      • 1970-01-01
      • 2022-03-30
      • 2021-12-28
      • 1970-01-01
      相关资源
      最近更新 更多