【问题标题】:Fast C comparison快速 C 比较
【发布时间】:2011-10-02 09:27:00
【问题描述】:

作为协议的一部分,我收到以下格式的 C 字符串:
字 * 字
其中两个 WORD 是相同的给定字符串。
而且, * - 是任何可打印字符的字符串,不包括空格!

所以以下都是合法的:

  • WORD asjdfnkn WORD
  • 字 234kjk2nd32jk 字

以下是非法的:

  1. WORD akldmWORD
  2. WORD asdm zz WORD
  3. NOTWORD admkas WORD
  4. NOTWORD admkas NOTWORD

其中 (1) 缺少尾随空格; (2)有3个以上的空格; (3)/(4) 不要以正确的字符串 (WORD) 打开/结束。

当然,这可以非常直接地实现,但是我不确定我在做什么是最有效的。 注意: WORD 是为整个运行预设的,但可能会因运行而异。

目前我正在将每个字符串与“WORD”相对应。 如果手动检查(逐个字符)在字符串上运行,则检查第二个空格字符。
[如果找到] 然后我用“WORD”strcmp(一路)。

很想听听您的解决方案,重点是效率,因为我将实时处理数百万篇论文。

【问题讨论】:

  • 可以接收非法格式吗?
  • 不知道在您的情况下是否可行,但如果您也可以获得传入的字符串的长度,您可以避免遍历整个字符串以找出是否存在“ WORD " 结尾的子字符串。然后你可以进行 O(1) 测试。
  • @Jeremy:他仍然需要遍历字符串来检查是否有多余的空间。
  • @yi_H:不幸的是我可以。这就是我要检查的内容:字符串是否格式错误。
  • @Jeremy:假设我能得到'n',你会怎么做?

标签: c comparison substring


【解决方案1】:

WORD 是 4 个字符,使用 uint32_t 可以进行快速比较。您将需要一个不同的常数,具体取决于系统字节序。其余的似乎都很好。

由于 WORD 可以更改,因此您必须根据 WORD 的长度预先计算所需的 uint32_t、uint64_t...。

从描述中不确定,但如果你相信来源,你可以只吃前 n+1 和最后 n+1 个字符。

【讨论】:

  • 我认为 OP 使用“WORD”来代表任意词。
  • ...只有当第二个 WORD 是第一个的 mod4 边界时它才会起作用,而他提供的示例中并非如此......
【解决方案2】:

我想说,看看Handbook of Exact String-Matching Algorithms 中的算法,比较复杂性并选择你最喜欢的算法,然后实施它。

或者你可以使用一些现成的实现。

这里有一些非常经典的算法用于在另一个字符串中搜索字符串:

KMP(Knuth-Morris-Pratt)

Rabin-Karp

Boyer-Moore

希望这会有所帮助:)

【讨论】:

  • 这里不需要或不需要字符串搜索算法,因为子字符串的预期位置是已知的。剩下要做的就是比较它。
  • 不是一个解决方案,但提供的 pdf 似乎非常有用 [而且读起来有点有趣]。为该来源+1! 10x
【解决方案3】:

在最短的代码和最快的实现之间可能需要权衡取舍。选择是:

  1. 正则表达式^WORD \S+ WORD$(需要正则表达式引擎)

  2. "WORD " 上的strchr 和“WORD”上的strrchr 带有很多杂乱的检查(不推荐)

  3. 逐个字符遍历整个字符串,跟踪您所处的状态(扫描第一个单词,扫描第一个空格,扫描中间,扫描最后一个空格,扫描最后一个单词,等待字符串结尾)。

选项 1 需要最少的代码,但在接近尾声时回溯,选项 2 没有可取之处。我认为您可以优雅地执行选项 3。使用状态变量,它看起来还不错。请记住根据单词的长度和整个字符串的长度手动输入最后两个状态,这样可以避免正则表达式最有可能出现的回溯。

【讨论】:

  • 1 & 2 没有解决问题中提出的效率问题。
  • 没错。我已经确定了答案。
【解决方案4】:
bool check_legal(
        const char *start, const char *end,
        const char *delim_start, const char *delim_end,
        const char **content_start, const char **content_end
) {
  const size_t delim_len = delim_end - delim_start;
  const char *p = start;

  if (start + delim_len + 1 + 0 + 1 + delim_len < end)
    return false;

  if (memcmp(p, delim_start, delim_len) != 0)
    return false;
  p += delim_len;

  if (*p != ' ')
    return false;
  p++;

  *content_start = p;
  while (p < end - 1 - delim_len && *p != ' ')
    p++;
  if (p + 1 + delim_len != end)
    return false;
  *content_end = p;
  p++;

  if (memcmp(p, delim_start, delim_len) != 0)
    return false;

  return true;
}

下面是如何使用它:

const char *line = "who is who";
const char *delim = "who";
const char *start, *end;

if (check_legal(line, line + strlen(line), delim, delim + strlen(delim), &start, &end)) {
  printf("this %*s nice\n", (int) (end - start), start);
}

(都是未经测试的。)

【讨论】:

    【解决方案5】:

    你有介绍过吗?

    这里没有太多收获,因为您正在进行基本的字符串比较。如果您想获得最后百分之几的性能,我会将 str... 函数更改为 mem... 函数。

    char *bufp, *bufe; // pointer to buffer, one past end of buffer
    if (bufe - bufp < wordlen * 2 + 2)
        error();
    if (memcmp(bufp, word, wordlen) || bufp[wordlen] != ' ')
        error();
    bufp += wordlen + 1;
    char *datap = bufp;
    char *datae = memchr(bufp, ' ', bufe - buf);
    if (!datae || bufe - datae < wordlen + 1)
        error();
    if (memcmp(datae + 1, word, wordlen))
        error();
    // Your data is in the range [datap, datae).
    

    性能提升可能不那么引人注目。您必须检查缓冲区中的每个字符,因为每个字符 可能 是空格,并且分隔符 可能 中的任何字符都是错误的。将循环更改为memchr 很巧妙,但现代编译器知道如何为您做到这一点。将strncmpstrcmp 更改为memcmp 也可能可以忽略不计。

    【讨论】:

    • 好点 memcmp() 而不是 strcmp()strncmp()
    • 正是医生吩咐的!哇!非常感谢!这些见解是我首先提出这个问题的原因! 10 倍!
    • 我检查了 glibc 参考实现中的 memcmp/strcmp 代码。很清楚为什么 memcmp 更好一点。 memcmp 将给定的内存区域视为一个长数组(4/8 字节整数)。在每次比较中,它比较 4/8 个字节,而不是传统的一次 1 个字节。这太棒了!
    • @Trevor:那一定是通用版本。您的系统实际上会在运行时使用 strcmp 的 SSE4 或 SSSE3 版本。
    【解决方案6】:

    你知道要检查的字符串有多长吗?如果没有,那么您的能力有限。如果您确实知道字符串有多长,则可以加快速度。您尚未确定“*”部分必须至少为一个字符。您还没有规定是否允许使用制表符,换行符,或者......它只是字母数字(如您的示例)还是允许标点符号和其他字符?控制字符?

    您知道 WORD 有多长,并且可以预先构建开始和结束标记。函数error() 报告错误(但您需要报告)并返回false。测试函数可能是bool string_is_ok(const char *string, int actstrlen);,成功返回true,出现问题返回false

    // Preset variables characterizing the search
    static int  wordlen    = 4;
    static int  marklen    = wordlen + 1;
    static int  minstrlen  = 2 * marklen + 1;  // Two blanks and one other character.
    static char bword[]    = "WORD ";          // Start marker
    static char eword[]    = " WORD";          // End marker
    static char verboten[] = " ";              // Forbidden characters
    
    bool string_is_ok(const char *string, int  actstrlen)
    {
        if (actstrlen < minstrlen)
            return error("string too short");
        if (strncmp(string, bword, marklen) != 0)
            return error("string does not start with WORD");
        if (strcmp(string + actstrlen - marklen, eword) != 0)
            return error("string does not finish with WORD");
        if (strcspn(string + marklen, verboten) != actstrlen - 2 * marklen)
            return error("string contains verboten characters");
        return true;
    }
    

    如果您想要保证,您可能无法大幅减少测试。根据字母表中的限制,变化最大的部分是strcspn() 行。对于一小部分禁用字符来说,这相对较快;随着禁止字符数的增加,它可能会变慢。如果只允许使用字母数字,则有 62 个 OK 和 193 个不 OK 字符,除非您也将一些高位集字符计为字母。那部分可能会很慢。您可能会使用一个自定义函数做得更好,该函数采用起始位置和长度并报告所有字符是否正常。这可能是这样的:

    #include <stdbool.h>
    
    static bool ok_chars[256] = { false };
    
    static void init_ok_chars(void)
    {
        const unsigned char *ok = "abcdefghijklmnopqrstuvwxyz...0123456789";
        int c;
        while ((c = *ok++) != 0)
            ok_chars[c] = 1;
    }
    
    static bool all_chars_ok(const char *check, int numchars)
    {
        for (i = 0; i < numchars; i++)
            if (ok_chars[check[i]] == 0)
                return false;
        return true;
    }
    

    然后您可以使用:

    return all_chars_ok(string + marklen, actstrlen - 2 * marklen);
    

    代替对strcspn()的调用。

    【讨论】:

      【解决方案7】:

      这应该在 O(n) 时间内返回真/假条件

      int sameWord(char *str)
      {
        char *word1, *word2;
        word1 = word2 = str;
      
        // Word1, Word2 points to beginning of line where the first word is found
      
        while (*word2 && *word2 != ' ') ++word2; // skip to first space
        if (*word2 == ' ') ++word2; // skip space
      
        // Word1 points to first word, word2 points to the middle-filler
      
        while (*word2 && *word2 != ' ') ++word2; // skip to second space
        if (*word2 == ' ') ++word2; // skip space
      
        // Word1 points to first word, word2 points to the second word
      
        // Now just compare that word1 and word2 point to identical strings.
      
        while (*word1 != ' ' && *word2)
           if (*word1++ != *word2++) return 0; //false
        return *word1 == ' ' && (*word2 == 0 || *word2 == ' ');
      }
      

      【讨论】:

      • 遍历字符串时看不到 O(1)。
      • 公平评论:O(n) :-) 无论如何,这是在唯一知道给定字符串的情况下可以完成的最快速度。任何 strcmp/memcmp 解决方案都不止一次地查看数据,并且它们调用一个函数调用,这会进一步减慢它,除非编译器能够内联它。
      • 我的解决方案只查看每个字节一次,不需要更多。函数调用只针对标准库的知名函数,至少 GCC 内联得很好。
      • 顺便说一句:当你到达第一个空格时,你应该word2++
      • @Roland Illig -- 解决方案应该是正确的,word2指针应该在开始比较之前前移到第二个空格。
      【解决方案8】:

      使用 STL 查找空格的数量..如果它们不是两个显然字符串是错误的..并且使用 find(algorithm.h) 你可以得到两个空格和中间单词的位置!检查开头和结尾的WORD!你完成了..

      【讨论】:

        【解决方案9】:

        如果您的“填充”应仅包含 '0'-'9'、'A'-'Z' 和 'a'-'z' 并且采用基于 ASCII 的某种编码(如大多数基于 Unicode 的编码),那么您可以在其中一个循环中跳过两次比较,因为大写字符和次要字符之间只有一点不同。 而不是

           ch>='0' && ch<='9' && ch>='A' && ch<='Z' && ch>='a' && ch<='a'
        

        你得到

           ch2 = ch & ~('a' ^ 'A')
        
           ch>='0' && ch<='9' && ch2>='A' && ch2<='Z'
        

        但是你最好看看你的编译器生成的汇编代码并做一些基准测试,这取决于计算机架构和编译器,这个技巧可能会产生更慢的代码。

        如果与您的计算机上的比较相比,分支成本较高,您还可以将 &amp;&amp; 替换为 &amp;。但是大多数现代编译器在大多数情况下都知道这个技巧。

        另一方面,如果您测试来自某个大字符编码的任何可打印字形,那么测试空白字形而不是可打印字形很可能会更便宜。

        另外,专门为运行代码的计算机进行编译,不要忘记任何一代的调试代码。

        已添加:

        除非值得,否则不要在扫描循环中调用子程序。

        无论您使用什么技巧来加速循环,如果您必须在其中一个循环中进行子例程调用,它都会减少。使用编译器内联到代码中的内置函数很好,但是如果你使用类似外部正则表达式库的东西并且你的编译器无法内联这些函数(gcc 可以做到这一点,有时,如果你要求它),然后进行该子例程调用将打乱大量内存,更糟糕的是,在不同类型的内存(寄存器、CPU 缓冲区、RAM、硬盘等)之间进行混洗,并且可能会弄乱 CPU 预测和管道。除非您的 text-sn-ps 很长,以至于您花费大量时间来解析它们中的每一个,并且子例程足以补偿调用成本,否则不要这样做。一些用于解析的函数使用回调,它可能比从循环中进行大量子例程调用更有效(因为该函数可以在一次扫描中扫描多个模式匹配并将多个回调聚集在关键循环之外) ,但这取决于其他人如何编写该函数,并且基本上与您进行调用相同。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-02-07
          • 1970-01-01
          • 2011-01-22
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多