【问题标题】:Optimising a naive string search in C优化 C 中的简单字符串搜索
【发布时间】:2016-05-07 23:57:19
【问题描述】:

作为高性能计算课程的一部分,我正在尝试尽可能加快 C 语言中的简单字符串搜索,我想知道是否有任何我错过的明显可以做的事情我当前的迭代,或者是否有更好的方向。

一些限制是模式必须从左到右搜索,每个字符必须单独检查,并且需要基于for循环。

到目前为止,我已经将 999 的模式所花费的时间减少了 9,999,999 的文本中的 B 后跟 B 从 ~18 秒到 ~9 秒,但我不确定它是否可以考虑到上述限制,速度会更快。

目前的代码是:

int match(char** text, int n, char** pattern, int m){
    int i, j, last = n-m;

    for(i = 0; i <= last; i++){
        for(j = 0; j < m; j++){
            if((*text)[i+j] != (*pattern)[j]){
                break;
            }
        }

        if(j == m){
            return i;
        }
    }
    return -1;
}

其中text是要搜索的文本,pattern是要搜索的模式,n是文本的长度,m是模式的长度。

编辑:这是朴素字符串搜索算法的实现。 https://en.wikipedia.org/wiki/String_searching_algorithm#Na.C3.AFve_string_search

编辑 2:@RadLexus 想法后的代码从 char** 更改为 char*(快 25%):

int match(char* text, int n, char* pattern, int m){
    int i, j, last = n-m;

    for(i = 0; i <= last; i++){
        for(j = 0; j < m; j++){
            if(text[i+j] != pattern[j]){
                break;
            }
        }

        if(j == m){
            return i;
        }
    }
    return -1;
}

【问题讨论】:

  • 我不确定我是否理解你的任务,但你可以使用if((*text)[i+j] != (*pattern)[j]){ i = i+j; break; } 来避免不必要的迭代
  • @RadLexus 可能是为了确保我们坚持简单的字符串搜索,而不是使用 Boyer-Moore 或 KMP。
  • 嗯。原型是给你的吗?我想知道双重间接是否会减慢速度,尤其是。在紧密的循环内。对于简单的字符串,一个char * 就足够了。
  • last 声明为常量。 const int last = n-m; 可能暗示编译器在循环的每次迭代中将 last 保存在寄存器中。
  • 是关于模式 AAAA...B 还是关于任意模式的问题?

标签: c string optimization pattern-matching


【解决方案1】:

通过这样做,我获得了 10 倍的加速:

for (int i = 0; i <= last; i++) {
    if (memcmp(&text[i], pattern, m) == 0) {
        return i;
    }
}

现在您可能会抱怨这不再使用“for”循环,因此不是解决方案。但是memcmp 使用循环,如果您愿意,您可以将其实现提升到您的代码中。至于为什么这么快,我们这​​里有一个很好的答案:Why is memcmp so much faster than a for loop check?

【讨论】:

  • 这很可能违反了“每个字符必须单独检查”的要求(这样做有很大的好处)。它也可能违反了问题的意图。为什么不使用strstr
  • @RadLexus:我在回答中直接提到了这个问题。是的,memcmp() 可能会被取缔。但是如果需要,您可以简单地将其实现技巧复制到您的代码中。我们没有理由每次都重新发明这个轮子一个操作码。
  • @RadLexus:memcmp 很可能是矢量化的,因此排除(甚至重写)。
【解决方案2】:

要仅使用循环,删除数组索引以支持指针可能会提供另一个加速:

int match(char* text, int n, char* pattern, int m){
    char *ptext= text, *pend= text+n, *ptext2;
    char *ppat, *ppatend;

    if (n<m) return -1;

    for (; ptext<pend; ptext++) {
        ptext2= ptext; ppat= pattern; ppatend= ppat+m;
        for (; ppat<ppatend; ppat++, ptext2++) {
            if (*ptext2 != *ppat) {
                break;
            }
        }
        if (ppat==ppatend) {
            return (ptext-text);
        }
    }
    return -1;
}

【讨论】:

  • 一个好的优化器会选择最快的实现,无论访问是通过索引还是指针解引用完成。我敢打赌不会有可衡量的差异。
  • @Yves-daoust, 1) 你必须检查汇编器以了解你是否有一个“好的”优化器,我怀疑它会将数组索引变成指针; 2)他的练习是优化他的搜索,所以他应该学习/知道用指针替换索引;他不应该学会依赖可能并不总是可用或“好”的优化器。
  • 我同意@YvesDaoust:除非你真的知道你想要什么asm,否则这只是个坏主意。你的版本has an inner loop that's 5 uops instead of 6, on Intel Haswell, with gcc 5.3,因为索引寻址模式不能在 SnB 系列微架构上进行微融合。更有趣的是使用负数组索引向零计数,以潜在地从循环开销中删除 cmp 指令。 (所以inc/jcc 可以进行微融合。)有可能将内部循环降低到 4 个融合域 uop,将其加速到每个时钟一个。
  • 一个更标准的方法是展开一点。即使是 gcc -funroll-loops 也可能会有所帮助,但源级展开可能会帮助 gcc 做得更好。
猜你喜欢
  • 1970-01-01
  • 2013-09-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-07-16
  • 2012-05-24
  • 2010-10-26
  • 2014-07-12
相关资源
最近更新 更多