确定数字是否出现在字符串中的最快方法是什么?

时间:2009-04-15 18:20:59

标签: c performance string

这个简单的解决方案很快就浮现在我脑海中。

#include <ctype.h>

int digit_exists_in
(
    const char *s
)
{
    while (*s)
    {
        if (isdigit(*s))
        {
            return 1;
        }
        else
        {
            s++;
        }
    }

    return 0;
}

int main(void)
{
    int foundDigit = digit_exists_in("abcdefg9ijklmn");

    return 0;
}

可以应用哪些其他技术来提高速度?

搜索的实际字符串是可变长度,字符本身是ASCII,而不是完整的字符集。字符串是NUL终止的。

16 个答案:

答案 0 :(得分:18)

顺便说一下,

liw.fi 是对的。我对此感到有些惊讶,因为strcspn必须比isdigit()方法做更普遍的问题,但似乎是这样的:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <assert.h>

#define NTESTS 10000
#define TESTSIZE 10000

char stest1[TESTSIZE];
char stest2[TESTSIZE];

int test_isdigit(char *s) {
    while (*s) {
        if (isdigit(*s)) return 1;
        s++;
    }
    return 0;
}

int test_range(char *s) {
    while (*s) {
        if ((*s >= '0') && (*s <= '9')) return 1;
        s++;
    }
    return 0;
}

int test_strcspn(char *s) {
    return s[strcspn(s, "0123456789")] != '\0';
}

int main(int argc, char **argv) {
    long int i;
    for (i=0; i<TESTSIZE; i++) {
        stest1[i] = stest2[i] = 'A' + i % 26;
    }
    stest2[TESTSIZE-1] = '5';

    int alg = atoi(argv[1]);

    switch (alg) {
        case 0:        
            printf("Testing strcspn\n");
            for (i=0; i<NTESTS; i++) {
                assert(test_strcspn(stest1) == 0);
                assert(test_strcspn(stest2) != 0);
            }
            break;
        case 1:
            printf("Testing isdigit() loop\n");
            for (i=0; i<NTESTS; i++) {
                assert(test_isdigit(stest1) == 0);
                assert(test_isdigit(stest2) != 0);
            }
            break;
        case 2:
            printf("Testing <= => loop\n");
            for (i=0; i<NTESTS; i++) {
                assert(test_range(stest1) == 0);
                assert(test_range(stest2) != 0);
            }
            break;
        default:
            printf("eh?\n");
            exit(1);
    }    

    return 0;
}

在他们自己的游戏中击败标准库非常困难......(通常的警告适用 - YMMV)

$ gcc -O6 -Wall -o strcspn strcspn.c 

$ time ./strcspn 0
Testing strcspn

real    0m0.085s
user    0m0.090s
sys 0m0.000s

$ time ./strcspn 1
Testing isdigit() loop

real    0m0.753s
user    0m0.750s
sys 0m0.000s

$ time ./strcspn 2
Testing <= => loop

real    0m0.247s
user    0m0.250s
sys 0m0.000s

更新:为了好玩,我根据Mike Dunlavey的回答添加了一个位图查找版本:

char bitmap[256] = {
        /* 0x00 */ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        /* 0x10 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        /* 0x20 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        /* 0x30 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
};

int test_bitmap(char *s) {
    while (!bitmap[*(unsigned char *)s]) s++;
    return (*s);
}

略胜一筹(〜。170s)但仍无法触及strcspn!

答案 1 :(得分:12)

我首先使用适当的库函数strcspn,假设库已经过极端偏见的优化:

#include <string.h>
#include <stdio.h>

int digit_exists_in(const char *s)
{
    return s[strcspn(s, "0123456789")] != '\0';
}

int main(void)
{
    printf("%d\n", digit_exists_in("foobar"));
    printf("%d\n", digit_exists_in("foobar1"));
    return 0;
}

如果图书馆没有得到充分的优化,那么它是个好主意 将优化放入库中,这样每个人都可以从中受益。 (你有来源, 右?)

答案 2 :(得分:10)

没有更快的算法,但您可以查看每个指令的完整寄存器的字节值,或使用SIMD操作来加快速度。如果某个范围内有任何数字,或者如果SIMD操作在足够大的矢量上足够快,您可以使用掩码和零测试来查看它是否可行,您可以迭代测试特定数值的测试。字节向量比做字符比较快。

因此,例如,您可以执行以下操作:

byte buffer[8] = { 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30 };
uint64 *mask = (uint64 *) buffer; //this is just for clarity

if (*((uint64 *) s) & *mask) == 0)
    //You now don't need to do the < '0' test for the next 8 bytes in s

一些优化器可能足够智能,只需从上面的代码示例中为您完成此操作。

你最好比较一下TON的字节来考虑这个级别的优化。

答案 3 :(得分:7)

任何算法都是O(N)。

我认为isdigit已经非常有效了。

答案 4 :(得分:3)

真正的问题是这个功能最佳是多么重要?我说离开简单的解决方案并对其进行分析。如果它导致代码出现瓶颈,则应该只对其进行优化。

答案 5 :(得分:2)

从概念上讲,没有更快的方法。这假设你有一个字符串,其中数字的位置似乎是随机的。这会强制您搜索字符串中的每个项目以获得数字,因此从前到后可能会像其他任何搜索机制一样首先找到该数字。

答案 6 :(得分:2)

你可以去多线程,虽然这可能会为算法增加太多的复杂性,因为它已经非常快了。

答案 7 :(得分:2)

只是为了好玩,也许还有以下几点:

 // accumulator
unsigned char thereIsADigit = 0;
// lookup table
unsigned char IsDigit[256] = {0,0,0 ..., 1,1,1,1,1,1,1,1,1,0,0,0 ...};

// an unrolled loop, something like:
thereIsADigit |= IsDigit[s[0]];
thereIsADigit |= IsDigit[s[1]];
thereIsADigit |= IsDigit[s[2]];
thereIsADigit |= IsDigit[s[3]];
thereIsADigit |= IsDigit[s[4]];
thereIsADigit |= IsDigit[s[5]];
thereIsADigit |= IsDigit[s[6]];
thereIsADigit |= IsDigit[s[7]];
if (thereIsADigit) break;
s += 8;

在IBM 360上有一个"translate" instruction可以一步完成。

好的,好的,克里斯托弗史密斯的回答让我思考。假设您只使用7位ASCII。这是一种使用宽整数运算进行SIMD的方法。

假设C是一个包含4个字符的32位字。

 // compare by subtracting in 8-bit 2s complement arithmetic
( (C + ((0x3a3a3a3a ^ 0x7f7f7f7f) + 0x01010101)) // set high bit for any char <= '9'
  &
  (0x2f2f2f2f + ((C ^ 0x7f7f7f7f) + 0x01010101)) // set high bit for any char >= '0'
) // high bit is set for any char <= '9' and >= '0'
& 0x80808080 // look only at the high-order bits
// if any of these 4 bits is 1, there is a digit in C
// if the result is zero, there are no digits in C

它取决于每个字符的高位最初为零,因此进位到该位不会传播。 (我确信这可以简化。)

答案 8 :(得分:1)

如果你真的想减少开销时间并且不介意使其特定于char,那么你可以检查0到9之间的ascii值。

48到57十进制

这将删除堆栈调用。

我应该说查询表......

答案 9 :(得分:1)

正如其他人所说,你不能低于O(N)。

我可以想象一个具有对数速度的人为场景......假设您正在编写文本编辑器,它需要“此文件包含任何数字”功能。您可以保留文件中存在的所有唯一字符的排序数组,在​​每次击键时更新它,并使用二进制搜索进行查询。这可能超出了你的问题的范围(并且可以用几种更好的方式完成)。

答案 10 :(得分:1)

这是一个版本可能更快,但可能没有更快,但它处理一个NULL指针......

int digit_exists_in(const char *s)
{
    if (!s)
        return (0);
    while (*s)
        if (isdigit(*s++))
            return (1);
    return (0);
}

答案 11 :(得分:1)

参加测试程序并将我的探查器扔到它上面会产生以下结果。

      Count      %   % with           Time   Statement
                      child
-----------------------------------------------------------------------------------------------
                                             int test_isdigit(char *s)   
     20,000    0.0    0.0          2,160.4   {   
199,990,000   13.2    9.5     14,278,460.7       while (*s)  
                                                 {   
199,980,000   69.8   49.9     75,243,524.7            if (isdigit(*s)) return 1;  
199,970,000   17.0   12.1     18,312,971.5            s++;    
                                                 }   

     10,000    0.0    0.0          1,151.4       return 0;   
                                             }   

                                             int test_range(char *s)     
     20,000    0.0    0.0          1,734.2   {   
199,990,000   33.6    9.4     14,114,309.7       while (*s)  
                                                 {
199,980,000   32.2    9.0     13,534,938.6           if ((*s >= '0') && 
                                                        (*s <= '9')) return 1;   
199,970,000   34.2    9.5     14,367,161.9           s++;    
                                                 }   

     10,000    0.0    0.0          1,122.2       return 0;   
                                             }   

                                             int test_strcspn(char *s)   
     20,000    0.2    0.0          1,816.6   {   
     20,000   99.8    0.6        863,113.2       return s[strcspn(s, "0123456789")] 
                                                          == '0'; 
                                             }   
strcspn完成了这项工作。查看它的asm代码,我看到它构建了一个256位的位图,根据搜索字符设置位,然后处理字符串。

每次调用都会在堆栈上构建一次位图。

另一种方法是构建并保留位图,并且每次都重复使用它。

另一种方法是使用这些技术并行进行操作 克里斯史密斯谈到了。

目前strcspn就足够了。

答案 12 :(得分:0)

让人看看它。人类可以在O(1)时间内做到这一点,我们拥有比现代处理器更大的字数。

那就是说,你的方法的实际时间仍然会更好......现代核心和人类大脑之间的周期时间有什么不同。

答案 13 :(得分:0)

我可能错了,但可能有更快的方式。

快速输入字符串。

串行搜索的最佳时间为O(1),平均值为O(1 / 2n),最差情况为O(n)。

Quicksort具有最好的O(log n),意味着O(nlog n),更糟的情况是O(n ^ 2)。

问题是,一旦看到数字,就可以快速退出快速排序。如果快速排序实际完成,则数字将位于排序字符串的开头,因此您将在O(1)中找到它。

这里取得的成就是改变最佳,卑鄙和更糟糕的案例行为。快速排序会有更糟糕的最坏情况,但更好的平均行为。

答案 14 :(得分:0)

当然,你可以牺牲速度的准确性:

int digit_exists_in(const char* s)
{
    return 0;
}

该算法的复杂度为O(1),近似值为O((246/256)^ N)。

答案 15 :(得分:0)

内存预取

如果您的字符串非常长,请让编译器执行此操作,或者手动展开循环并放入内存预取指令或每个缓存行两个。

当CPU正在扫描时,内存控制器可以拉入下一行数据。

如果在创建字符串时保存字符串的长度,则可以跳过对NUL字节的所有检查,这意味着您可以展开循环以在更大的块中操作并减少比较和分支操作的数量,尽管目前的分支预测因素,老实说没有太大的区别。

即使有很好的CPU分支预测器,如果必须每次通过循环检查循环计数器以决定何时进行内存预取,循环将减慢,因此在这种情况下展开仍然有用。

分析反馈

为了获得最佳性能,CPU确实需要正确地提示分支,这就是分析反馈非常方便的地方。否则,编译器只是在做一些有根据的猜测。