【问题标题】:Fast string comparison in CC中的快速字符串比较
【发布时间】:2012-05-23 14:48:02
【问题描述】:

我目前有这种循环

while(1)
{
    generate_string(&buffer);

    for(int i = 0; i < filelines; i++)
    {
        if(strcmp(buffer,line[i]) == 0)
        {
           /*  do something  */
        }
    }
}

我有一个包含几百万个字符串的文件(希望很快会减半),所有这些字符串的数量都存储在 filelines

line[i] 基本上是字符串本身的存储位置。

目前,由于这百万个字符串的比较,函数 generate_string(&buffer); 每秒执行大约 42 次。 有没有更快的方法在 C 中进行字符串比较?

【问题讨论】:

  • @KingsIndian:不,因为这里真正的问题不是“如何比较两个字符串”,而是“如何测试一个字符串是否包含在大量字符串中”。
  • 只有当字符串的大小相等时,才可以执行 if((buffer[0] == line[0]) && (buffer[1] == line[1]) && ...)。这比调用 strcmp() 更快。
  • 我刚刚在 wakkerbot 上运行了一个配置文件:它使用 200 毫秒在 500K 的已知单词词典中进行 2M 次查找。在匹配的哈希表条目中包含最终的 strcmp()。

标签: c string compare


【解决方案1】:

strcmp 通常由所有供应商优化。但是,如果您对此不满意,可以尝试:

  • 查找Burst Tries
  • 使用后缀树进行快速字符串比较 -- 参见 this 文章
  • 根据应用程序中字符串的大小,您可以编写自定义字符串比较器。例如:GNU libc 曾经对小字符串进行过这种优化,他们将小于 5 个字节的字符串作为整数进行测试。 MS cl 也对小字符串进行了一些优化(请查看)。

但更重要的是确保strcmp 是您的真正瓶颈。

【讨论】:

  • 是的,strcmp 是瓶颈。删除 strcmp 调用,该函数每秒执行超过一千次,在某些情况下甚至 1100 次。
  • @dirkgently:您的“查看这篇文章”链接不再链接到任何文章,而只是教授的主页。
  • @dirkgently,您能否提供“MS cl ...”声明的链接?而且,即使对于“ GNU libc 曾经有......”声明?谢谢。
【解决方案2】:

我可以向你保证,strcmp 函数绝对不是瓶颈。通常,strcmp 进行了很好的优化,可以根据架构对超过 4/8 字节的字符串进行 32 位或 64 位比较。 newlib 和 GNU libc 都这样做。但是,即使您要查看两个字符串中的每个字节 20 次,也没有这里选择的算法和数据结构那么重要。

真正的瓶颈是 O(N) 搜索算法。文件中的单个 O(N log N) 传递可用于在适当的数据结构(无论是普通的 BST、trie 还是简单的排序数组)上进行 O(log N) 查找。

请耐心听我说——后面有很多数学。但我认为这是一个很好的机会来说明为什么算法和数据结构的选择有时比字符串比较的方法更重要。史蒂夫谈到了这一点,但我想更深入地解释一下。

在 N=1e6 时,log(1e6, 2) = 19.9,因此在理想数据结构上最多进行 20 次比较。

目前您正在对 O(N) 或 1e6 操作进行最坏情况搜索。

假设你只是构建一个插入时间为 O(log N) 的红黑树,然后插入 N 个项目,构建树的时间为 O(N log N)。所以这是构建树所需的 1e6 x 20 或 20e6 操作。

在您当前的方法中,构建数据结构是 O(N) 或 1e6 次操作,但最坏情况下的搜索时间也是 O(N)。因此,当您读取文件并仅执行 20 次搜索操作时,理论上最坏的情况是 21,000,000 次操作。相比之下,红黑树和 20 次搜索的最坏情况是 20,000,400 次操作,或 999,600 次操作比在未排序数组上的 O(N) 搜索要好。因此,在 20 次搜索时,您就处于更复杂的数据结构真正获得回报的第一个点。但是看看 1000 次搜索会发生什么:

未排序数组 = 初始化 + 1000 x 搜索时间 = O(N) + 1000 * O(N) = 1,000,000 + 2,000,000,000 = 2,001,000,000 次操作。

红黑 = 初始化 + 1000 x 搜索时间 = O(N log N) + 1000 * O(log N) = 20,000,000 + 20,000 = 20,020,000 次操作。

2,001,000,000 / 20,020,000 ~= O(N) 搜索的操作数的 100 倍。

在 1e6 次搜索时,即 (1e6 + 1e6 * 1e6) / (20e6 + 1e6 * 20 ) = 25,000 倍的操作。

假设您的计算机可以处理在 1 分钟内执行 log N 次搜索所需的 40e6 次“操作”。使用您当前的算法完成相同的工作需要 25,000 分钟或 17 天。或者另一种看待方式是,O(N) 搜索算法只能处理 39 次搜索,而 O(log N) 算法可以进行 1,000,000 次。而且你搜索的越多,它就越难看。

请参阅 Steve 的回复,并直接了解几种更好的数据结构和算法选择。我唯一需要注意的是,史蒂夫建议的qsort()可能具有 O(N*N) 的最坏情况复杂度,这比 O(N log N) 差得多得多你会得到一个堆排序或各种树状结构。

【讨论】:

    【解决方案3】:

    Optimization of Computer Programs in C

    在调用之前检查相关字符串的第一个字符可以节省一点时间。显然,如果第一个字符不同,则没有理由调用 strcmp 来检查其余字符。由于自然语言中字母的分布不均匀,因此收益不是 26:1,而更像是大写数据的 15:1。

    #define QUICKIE_STRCMP(a, b)  (*(a) != *(b) ? \  
      (int) ((unsigned char) *(a) - \
             (unsigned char) *(b)) : \
      strcmp((a), (b)))
    

    如果您使用的单词字典定义明确(意味着您不介意从 strcmp 返回值,而是 0==equal),例如,一组以相同前缀开头的命令行参数,例如: tcp-accept, tcp-reject 比你可以重写宏并做一些指针运算来比较不是第一个而是第 N 个字符,在这种情况下是第 4 个字符,例如:

       #define QUICKIE_STRCMP(a, b, offset) \
                (*(a+offset) != *(b+offset))\ ? -1 : strcmp((a), (b)))
    

    【讨论】:

    • 我真的怀疑比较第一个字符的宏是否会为现代编译器和库产生更好的结果。
    【解决方案4】:

    如果我正确地回答了您的问题,您需要检查一个字符串是否符合目前所读取的所有行。我建议在文件行中使用 TRIE 甚至更好的 Patricia tree。通过这种方式,您可以线性检查您的字符串是否存在,而不是遍历所有行(并且需要更多的努力 - 在哪里)。

    【讨论】:

      【解决方案5】:

      你已经在编译优化了,对吧?

      如果你有一个 Trie 或哈希表数据结构,可以随时使用,那么你应该这样做。

      如果做不到这一点,一个可能会加快速度的相当简单的更改是在开始生成要搜索的字符串之前对数组line 进行一次排序。然后在排序后的数组中对buffer进行二分搜索。这很容易,因为您需要的两个函数是标准的——qsortbsearch

      对已排序数组的二分搜索只需要对 log2(filelines) 字符串进行比较,而不是对文件行进行比较。因此,在您的情况下,每次调用generate_string 进行 20 次字符串比较,而不是几百万。根据您提供的数据,我认为您可以合理地预期它会快 20-25 倍,尽管我不保证。

      【讨论】:

      • 函数qsort() 顾名思义可能是一个快速排序,它具有 O(N*N) 最坏情况的性能。除非我确定 qsort() 在目标平台上的行为如何,否则我会选择平均速度较慢的,但在最坏情况下的 hepasort 或 smoothsort 上要快得多。
      • @Brian:如果你愿意的话。正如我所说,qsort 的优势在于它是标准的。如果我必须自己做这项工作,那么老实说,我可能宁愿写一个哈希表而不是堆排序 :-) 无论如何,与生成的字符串数量相比,启动时间是否重要并不完全清楚一旦我们启动并运行,每秒。如果启动时间并不重要,那么将qsort 实现为冒泡排序绝对没问题!
      • 经过验证的排序算法可能比散列函数更难搞砸,而糟糕的散列函数会让您回到最坏的 O(N) 搜索时间。
      • @Brian:djbhash 对我来说已经足够好了,但哈希表确实也有灾难性的最坏情况性能。一些分析是有序的,lines 中的字符串列表是否可能被恶意构建为快速排序和/或哈希杀手。如果您担心这类事情,那么您必须决定是编写自己的算法,还是只选择一个 qsort 可以抵抗的标准库。
      【解决方案6】:

      我使用strcmp() 和一个宏来逐字节比较基准。与strcmp() 相比,宏版本“非常”快得多。通常对于字符串比较,使用字节比较器宏要快得多,而不是strcmp()。 例如:

      #define str3_cmp(m, c0, c1, c2, c3) m[0] == c0 && m[1] == c1 && m[2] == c2 && m[3] == c3
      

      strcmp() 相比,这“非常”快。但是写下来是一件很痛苦的事情,而且您需要逐个字符地拆分字符串,所以我编写了一个方便的 PHP 脚本来为您生成它作为头文件。

      您可以在热循环中使用此字符串比较,您可以准确地知道要比较的char* 的大小。

      #!/usr/bin/php
      <?php
      function generate_macro($num) : string {
              $returner = "#define str".$num."cmp_macro(ptr, ";
              for($x = 0; $x < $num; $x++){
                      $returner .= "c".$x;
                      if($x != $num-1){ $returner .= ", "; }
              }
              $returner .= ") ";
              for($x = 0; $x < $num; $x++){
                      $returner .= "*(ptr+".$x.") == c".$x;
                      if($x != $num-1){ $returner .= " && "; }
              }
              return $returner;
      }
      function generate_static_inline_fn(&$generated_macro, $num) : string {
              $generated_macro .= "static inline bool str".$num."cmp(const char* ptr, const char* cmp)".
                                      "{\n\t\treturn str".$num."cmp_macro(ptr, ";
              for($x = 0; $x < $num; $x++){
                      $generated_macro .= " *(cmp+".$x.")";
                      if($x != $num-1){ $generated_macro .= ", "; }
              }
              $generated_macro .= ");\n}\n";
              return $generated_macro;
      }
      
      function handle_generation($argc, $argv) : void {
              $out_filename = $argv[$argc-1];
              $gen_macro = "";
              for($x = 0; $x < $argc-2; $x++){
                      $macro = generate_macro($argv[$x+1])."\n";
                      $gen_macro .= generate_static_inline_fn($macro, $argv[$x+1]);
              }
              file_put_contents($out_filename, $gen_macro);
      }
      handle_generation($argc, $argv);
      ?>
      

      这个脚本有两个参数。

      1. 您要比较的char* 的大小。
      2. 输出头文件名。

      示例:$ ./gen_faststrcmp.php 3 5 fast_strcmp.h 这将生成带有内容的fast_strcmp.h

      #define str3cmp_macro(ptr, c0, c1, c2) *(ptr+0) == c0 && *(ptr+1) == c1 && *(ptr+2) == c2
      static inline bool str3cmp(const char* ptr, const char* cmp){
                      return str3cmp_macro(ptr,  *(cmp+0),  *(cmp+1),  *(cmp+2));
      }
      #define str5cmp_macro(ptr, c0, c1, c2, c3, c4) *(ptr+0) == c0 && *(ptr+1) == c1 && *(ptr+2) == c2 && *(ptr+3) == c3 && *(ptr+4) == c4
      static inline bool str5cmp(const char* ptr, const char* cmp){
                      return str5cmp_macro(ptr,  *(cmp+0),  *(cmp+1),  *(cmp+2),  *(cmp+3),  *(cmp+4));
      }
      

      在你的代码中你可以像这样使用函数,

      const char* compare_me = "Hello";
      if(str5cmp(compare_me, "Hello")) { /* code goes here */ }
      

      【讨论】:

        【解决方案7】:

        我不知道有比调用strcmp 进行字符串比较更快的方法,但是您或许可以避免调用strcmp 这么多。使用哈希表存储您的字符串,然后您可以检查buffer 中的字符串是否在哈希表中。如果命中的索引在您“做某事”时很重要,则该表可以将字符串映射到索引。

        【讨论】:

          【解决方案8】:

          您可以尝试一些“便宜”的东西,例如基于第一个字符的筛选。如果第一个字符不匹配,则字符串不能相等。如果它们匹配,则调用 strcmp 来比较整个字符串。如果适合您的情况,您可能希望考虑更好的算法;例如,使用哈希表或类似的字符串表技术对文件/行进行排序并进行二进制搜索。

          【讨论】:

            【解决方案9】:

            在这种情况下,您可以通过二进制比较来解决问题,因为您的程序实际上并没有排序,而是比较是否相等。

            您还可以通过提前确定长度来提高比较速度(当然前提是它们变化足够大)。当这里的长度不匹配时,do something 不会发生。

            当然,这里的散列是另一个考虑因素,具体取决于您读取散列值的次数。

            【讨论】:

              【解决方案10】:

              这取决于字符串的长度。

              如果不是太长,可以尝试逐字节比较:

              str[0] == str2[0] && str[1] == str2[1] && str[2] == str2[2]
              

              否则,使用memcmp(),它会比较内存块。

              【讨论】:

                【解决方案11】:

                strcmp 用于常规字符串。但是如果字符串真的很长,你可以使用memcmp。它会比较内存块。

                【讨论】:

                  猜你喜欢
                  • 2013-08-22
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2010-11-12
                  相关资源
                  最近更新 更多