C语言中快速字符串比较

11

我目前有以下这个循环

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语言字符串比较方法?


1
@KingsIndian:不是的,因为这里真正的问题不是“如何比较两个字符串”,而是“如何测试一个字符串是否包含在大量字符串集合中”。 - Steve Jessop
只有当字符串的大小相等时,您才能执行if((buffer[0] == line[0]) && (buffer[1] == line[1]) && ...)。这比调用strcmp()更快。 - André A. G. Scotá
我刚刚在wakkerbot上运行了一个分析报告:它用200毫秒在一个已知单词500K字典中进行2M次查找。这包括在匹配哈希表条目时使用的最终strcmp()函数。 - wildplasser
11个回答

15

strcmp通常被所有供应商进行了优化。但是,如果您对此不满意,可以尝试以下方法:

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

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


是的,strcmp是瓶颈。移除strcmp调用后,该函数每秒执行超过一千次,甚至在某些情况下达到1100次。 - farmdve
@dirkgently:你的“看这篇文章”的链接不再指向任何文章,而只是教授的主页。 - Peter Hansen
@dirkgently,请问你能提供“MS cl ...”语句的链接吗?还有,“GNU libc曾经有过…”这个语句的链接也可以吗?谢谢。 - Vishwajith.K

6
我可以向您保证,strcmp函数绝对不是瓶颈。通常情况下,strcmp已经被优化得非常好了,并且可以对长于4/8字节的字符串进行32位或64位比较,具体取决于架构。新libc和GNU libc都是如此。但即使您要在两个字符串中每个字节上查看20次,也不像算法和数据结构选择所做的那样重要。
真正的瓶颈是O(N)搜索算法。可以在文件上进行单个O(N log N)遍历,以适当的数据结构(无论是普通BST、trie还是简单的排序数组)进行O(log N)查找。
请耐心阅读这里——接下来有很多数学内容。但我认为这是一个很好的机会,可以说明为什么算法和数据结构的选择有时比字符串比较方法更重要。Steve提到了这一点,但我想更深入地解释一下。
对于N=1e6,log(1e6, 2)=19.9,因此在理想的数据结构上进行20次比较。
目前,您正在进行O(N)的最坏情况搜索,即1e6个操作。
所以说,如果你只是建立一个红黑树,它的插入时间为O(log N),并且插入N个项目,那么构建树需要O(N log N)时间。因此,需要2000万个操作来构建树。
在您当前的方法中,构建数据结构是O(N),或者说1e6个操作,但是最坏情况下的搜索时间也是O(N)。因此,当您读取文件并进行20次搜索操作时,您将达到理论上的最坏情况2100万个操作。相比之下,使用红黑树和20次搜索的最坏情况为20000400个操作,比未排序数组上的O(N)搜索好999600个操作。因此,在20次搜索时,您已经到了更复杂的数据结构真正产生回报的第一点。但是看看在1000次搜索时会发生什么:
未排序数组=初始化+1000 x搜索时间=O(N)+1000 x O(N)=1000000 + 2000000000 = 2001000000个操作。
红黑树=初始化+1000 x搜索时间=O(N log N)+1000 x O(log N)=20000000 + 20000 = 20020000个操作。
O(N)搜索需要的操作数是O(log N)搜索的100倍。
在进行100万次搜索时,使用此算法需要(1e6 + 1e6 * 1e6) / (20e6 + 1e6 * 20 ) = 25,000倍的操作量。假设您的计算机可以处理执行log N个搜索需要的40e6个“操作”,那么使用当前算法完成相同的工作需要25,000分钟,即17天。换句话说,O(N)搜索算法在O(log N)算法1,000,000次搜索所需的时间内只能处理39次搜索,而且进行的搜索越多,情况就会变得越糟。请参阅Steve和dirkgently提供的几种更好的数据结构和算法。我唯一的额外提示是,由Steve推荐的qsort()可能具有最坏复杂度为O(N*N),这比堆排序或各种树形结构得到的O(N log N)差得多,请注意。

5

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 表示相等),比如一组以相同前缀开头的命令行参数,例如:tcp-accept、tcp-reject,那么您可以重写宏并进行指针算术运算来比较第 N 个字符,而不是第一个字符,例如:第四个字符。
   #define QUICKIE_STRCMP(a, b, offset) \
            (*(a+offset) != *(b+offset))\ ? -1 : strcmp((a), (b)))

4
我认为针对现代编译器和库进行首字母宏比较并不能得出更好的结果。 - manuell

2
如果我正确理解您的问题,您需要检查一个字符串是否在迄今为止读取的所有行中。我建议使用一棵 TRIE 树,甚至更好的是从文件行中使用 Patricia 树。这样,您可以线性地检查您的字符串是否存在(并且通过更多努力,找到位置),而不必遍历所有行。

1
你可以使用字节比较宏而不是 strcmp() 来实现非常快的字符串比较(标准 8 位 char),如果你事先知道字符串长度。我对比了字节比较宏和 glibc 的 strcmp(),宏版本明显优于 strcmp() 实现;它利用了 CPU 的 矢量处理器
示例:
#define str3_cmp(x, y0, y1, y2, y3) x[0] == y0 && x[1] == y1 && x[2] == y2 && x[3] == y3
static inline bool str3_cmp_helper(const char *x, const char *y) {
    return str3_cmp(x, *y, *(y + 1), *(y + 2), *(y + 3));
}

const char *i = "hola"; // dynamically generated (eg: received over a network)

if (str3_cmp_helper(i, "hola")) {
     /* do something */ 
} else { 
    /* do something else */ 
}

然而,编写这样的宏是很繁琐的,因此我已经包含了一个PHP脚本来生成宏。该脚本需要两个参数,(1)要比较的字符串长度(此参数为可变参数,因此您可以编写尽可能多的宏),以及(2)输出文件名。
#!/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);
?>

脚本示例:$ ./gen_faststrcmp.php 3 5 fast_strcmp.h

这将生成fast_strcmp.h,其中包含用于比较长度为3和5的字符串的宏:

#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 */ }

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

1

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

如果你有 Trie 或 hashtable 数据结构可供使用,那么最好使用它们。

如果没有,一个相当简单的改变可以加速程序,那就是在开始生成字符串搜索之前,对数组 line 进行一次排序。然后在排序后的数组中用二分查找来搜索 buffer。这很容易实现,因为你只需要使用两个标准函数——qsortbsearch

在已排序的数组中进行二分查找只需要进行约 log2(filelines) 次字符串比较,而不是 filelines 次。因此,在你的情况下,每次调用 generate_string 只需要进行大约 20 多次字符串比较,而不是几百万次。根据你提供的数据,我认为你可以合理地期望它的速度会快 20-25 倍,但我不能保证。


1
函数qsort()的名称暗示它可能是快速排序,其最坏情况下的性能为O(N*N)。除非我确定qsort()在目标平台上的行为,否则我会选择平均较慢但最坏情况下更快的hepasort或smoothsort。 - Brian McFarland
@Brian:如果你喜欢的话。正如我所说,qsort 的优点在于它是标准的。如果我必须自己完成这项工作,那么说实话,我宁愿编写哈希表而不是堆排序 :-) 无论如何,启动时间是否重要并不完全清楚,与我们运行时每秒生成的字符串数量相比。如果启动时间并不真的很重要,那么将 qsort 实现为冒泡排序就绰绰有余了! - Steve Jessop
2
一个经过验证的排序算法可能比散列函数更难出错,而糟糕的散列函数会使你回到最坏情况下的O(N)搜索时间。 - Brian McFarland
@Brian:djbhash 对我来说已经足够好了,但是确实哈希表也有灾难性的最坏情况性能。需要进行一些分析,看看 lines 中的字符串列表是否可能被恶意构造为快速排序和/或哈希杀手。如果你担心这种情况,那么你必须决定是编写自己的算法,还是选择一个标准库,其中 qsort 是抗攻击的。 - Steve Jessop

0

对于普通字符串,请使用strcmp。但如果字符串非常长,您可以使用memcmp。它将比较内存块。


0

这取决于字符串的长度。

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

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

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

0
我不知道比调用strcmp更快的字符串比较方法,但是你可以通过使用哈希表来存储你的字符串,从而避免频繁调用strcmp。然后,你可以检查buffer中的字符串是否在哈希表中。如果命中的索引在“做某事”时很重要,那么该表可以将字符串映射到索引。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接