【问题标题】:Faster than scanf?比scanf快?
【发布时间】:2012-01-18 22:25:05
【问题描述】:

我正在使用scanf("%d", &someint) 对正整数进行大量解析。因为我想看看 scanf 是否是一个瓶颈,所以我使用fread 实现了一个简单的整数解析函数,就像:

int result;
char c;

while (fread(&c, sizeof c, 1, stdin), c == ' ' || c == '\n')
    ;

result = c - '0';
while (fread(&c, sizeof c, 1, stdin), c >= '0' || c <= '9') {
     result *= 10;
     result += c - '0';
}

return result;

但令我惊讶的是,这个函数的性能(即使有内联)大约差了 50%。不应该有可能改进 scanf 的特殊情况吗? fread 不应该很快吗(附加提示:整数是(edit: most)1 或 2 位数字)?

【问题讨论】:

  • 你不是说c &gt;= '0' &amp;&amp; c &lt;= '9'
  • scanf 可能已被编译器优化。 fread 几乎肯定没有优化为使用 STDIN 作为输入。
  • 至少你应该使用fgetc...
  • fread() 肯定不打算一次读取一个字符。大概甚至 fgetc() 表现更好。
  • 从挑剔的角度来看,我建议使用isdigit() 而不是c&gt;='0' &amp;&amp; c&lt;='9',因为数字可能并非在每个字符集中都是连续的,例如EBCDIC。

标签: c input scanf numeric-input


【解决方案1】:

我遇到的开销不是解析本身,而是对fread 的多次调用(与fgetc 和朋友一样)。对于每次调用,libc 必须锁定输入流以确保两个线程不会踩到对方的脚。锁定是一项非常昂贵的操作。

我们正在寻找的是一个为我们提供缓冲输入的函数(重新实现缓冲太费力了),但又能避免 fgetc 的巨大锁定开销。

如果我们能保证只有一个线程在使用输入流,我们可以使用unlocked_stdio(3)中的函数,比如getchar_unlocked(3)。这是一个例子:

static int parseint(void)
{
    int c, n;

    n = getchar_unlocked() - '0';
    while (isdigit((c = getchar_unlocked())))
        n = 10*n + c-'0';

    return n;
}

以上版本不检查错误。但它保证会终止。如果需要错误处理,最后检查 feof(stdin)ferror(stdin) 可能就足够了,或者让调用者去做。

我最初的目的是在 SPOJ 提交编程问题的解决方案,其中输入只有空格和数字。所以还有改进的余地,即isdigit检查。

static int parseint(void)
{
    int c, n;

    n = getchar_unlocked() - '0';
    while ((c = getchar_unlocked()) >= '0')
        n = 10*n + c-'0';

    return n;
}

无论是在性能方面,还是在便利性和可维护性方面,都很难超越这个解析例程。

【讨论】:

    【解决方案2】:

    您将能够通过缓冲显着改进您的示例 - 将大量字符读入内存,然后从内存版本中解析它们。

    如果您从磁盘读取数据,您的缓冲区可能会提高性能,因为您的缓冲区是块大小的倍数。

    编辑:您可以让内核为您处理这个问题,方法是使用mmap 将文件映射到内存中。

    【讨论】:

    • 我想对此表示怀疑。我很确定 libc 会自行缓冲。除非您使用系统调用 (read(2)/write(2)),否则我认为实现自己的缓冲区可能会减慢速度...
    • 试一试 - 很高兴错了 :) 我记得手动缓冲大大加快了曾经是 getc() 的代码,但可能是标准输入,它不是块缓冲的。您也可以尝试给setvbuf() 一个更大的缓冲区。
    • @JoSo:只是为了好玩,我读取了一个 372MB 的文件,每次调用 fread 1 个字节,花了将近 30 秒。一次读取 4K 需要 1.3 秒。当然,你已经想通了。尽管如此,接受这个答案可能会很好。
    • 这并不完全是我的问题的答案,我的问题是在更窄的领域击败 scanf (scanf(%d, ...) 也不应该做缓冲!)。无论如何都接受了,因为没有人愿意写答案;-)
    • @JoSo 是的,stdio 会缓冲,但更有可能的瓶颈只是对 fgetc 的 3.72 亿次调用,而不是双缓冲的任何副作用。
    【解决方案3】:

    这是我使用的东西。

     #define scan(x) do{while((x=getchar())<'0'); for(x-='0'; '0'<=(_=getchar()); x=(x<<3)+(x<<1)+_-'0');}while(0)
     char _;
    

    但是,这仅适用于整数。

    【讨论】:

    • 这很好用!虽然,我建议使用 getcx() 或 getchar_unlocked() 而不是 getchar()。
    • char _; 是做什么的?
    • @light2yellow char _; 定义了一个变量,其名称为下划线字符_,用于宏本身,例如(_=getchar())。目的是要有一个全局变量,该变量在宏定义的地方定义,然后宏使用。我可能会使用static char _; 来限制文件或编译单元的范围。对于多线程应用程序可能不是一个好主意。真的应该是一个函数,而不是一个宏。读取第一个数字的输入,然后将一系列数字转换为整数值。在第一个非数字处停止阅读。
    【解决方案4】:

    根据你的说法,我得出以下事实:

    • 数字在 0-99 范围内,占 10+100 个不同的字符串(包括前导零)
    • 您相信您的输入流符合某种规范并且不会包含任何意外的字符序列

    在这种情况下,我会使用查找表将字符串转换为数字。给定一个字符串 s[2],查找表的索引可以通过 s[1]*10 + s[0] 计算,交换数字并利用'\0' 等于 ASCII 中的0 的事实。

    然后,您可以通过以下方式读取您的输入:

    // given our lookup method, this table may need padding entries
    int lookup_table[] = { /*...*/ };
    
    // no need to call superfluous functions
    #define str2int(x) (lookup_table[(x)[1]*10 + (x)[0]])
    
    while(read_token_from_stream(stdin, buf))
            next_int = str2int(buf);
    

    在今天的机器上,很难想出更快的技术。我的猜测是,这种方法的运行速度可能比任何基于 scanf() 的方法快 2 到 10 倍。

    【讨论】:

    • 对不起,我的意思是“大多数”数字是 1 或 2 位数字。无论如何,使用查找表的意义何在?我看不到你在哪里节省周期。 (您正在从我的解决方案中保存 c - '0' 部分,但另一方面您有更多重定向)
    • Jo So:我还保存了 c &gt;= '0' || c &lt;= '9'c == ' ' || c == '\n' 部分,每个部分最多三个指令。但不可否认,这些操作的真正子集需要我用read_token_from_stream() 缩写。
    猜你喜欢
    • 2012-12-21
    • 1970-01-01
    • 2010-11-05
    • 2017-01-11
    • 1970-01-01
    • 2017-06-02
    • 2021-08-07
    • 2018-08-04
    • 1970-01-01
    相关资源
    最近更新 更多