在 C 中使用 fread 从标准输入缓冲读取

Posted

技术标签:

【中文标题】在 C 中使用 fread 从标准输入缓冲读取【英文标题】:Buffered reading from stdin using fread in C 【发布时间】:2011-01-23 04:46:37 【问题描述】:

我正在尝试通过在`_IOFBF~ 模式下使用setvbuf 来有效地读取stdin。我是缓冲的新手。我正在寻找工作示例。

输入以两个整数 (n,k) 开头。接下来的 n 输入行包含 1 个整数。目的是打印有多少整数可以被k 整除。

#define BUFSIZE 32
int main()
  int n, k, tmp, ans=0, i, j;
  char buf[BUFSIZE+1] = '0';
  setvbuf(stdin, (char*)NULL, _IONBF, 0);
  scanf("%d%d\n", &n, &k);
  while(n>0 && fread(buf, (size_t)1, (size_t)BUFSIZE, stdin))
    i=0; j=0;
    while(n>0 && sscanf(buf+j, "%d%n", &tmp, &i))
    //printf("tmp %d - scan %d\n",tmp,i); //for debugging
      if(tmp%k==0)  ++ans;
      j += i; //increment the position where sscanf should read from
      --n;
    
  
  printf("%d", ans);
  return 0;

问题是如果数字在边界处,缓冲区buf 将从2354\n 读取23,而它应该读取2354(它不能)或什么都没有。

我该如何解决这个问题?


编辑Resolved now (with analysis).

编辑Complete Problem Specification

【问题讨论】:

【参考方案1】:

只有当从stdin 读取返回EOF 时,最外层的while() 循环才会退出。只有在到达输入文件的实际文件结尾时,或者写入输入管道的进程退出时,才会发生这种情况。因此,printf() 语句永远不会被执行。我认为这与对setvbuf() 的调用无关。

【讨论】:

我已经知道你在这里回答了什么,但是我该如何停止恐惧呢?而且我还没有说明问题是由于 setvbuf 造成的。 好的,所以如果我理解正确,您将 stdin 上的缓冲区大小设置为某个值,然后从中读取。您可能应该省略对 fread() 的调用,并将 sscanf() 调用更改为 fscanf()。第一个这样的调用应该将 BUFSIZE 字节读入流的(内部)缓冲区,然后后续调用一次将其发送给您一行。 您是否完整阅读了问题?请阅读它,请不要在这样做之前发布答案。 我确实完全阅读了你的问题,所以我随意提出一个更好的方法 - 不要使用 fread() 这就是重点:)。我必须使用 fread 来消耗大量输入。【参考方案2】:

Mabe 也看看这个 getline 实现:

http://www.cpax.org.uk/prg/portable/c/libs/sosman/index.php

(用于从流中获取长度未知的一行数据的 ISO C 例程。)

【讨论】:

【参考方案3】:

在看到n 整数后,您可以使用n 的值停止读取输入。

将外部while循环的条件改为:

while(n > 0 && fread(buf, sizeof('1'), BUFSIZE, stdin))

并将内部的主体更改为:


  n--;
  if(tmp%k == 0)  ++ans;


您仍然遇到的问题是,因为您从未在内部 while 循环中调整 bufsscanf 会一遍又一遍地读取相同的数字。

如果您切换到使用strtol() 而不是sscanf(),那么您可以使用endptr 输出参数在读取数字时在缓冲区中移动。

【讨论】:

您还需要更改sscanf 字符串,请参阅更新后的答案。 我现在正在使用 n>0 && sscanf(buf,"%d",&tmp),虽然它停止了,但打印的答案是错误的。而且每个数字都在不同的行,所以我猜是 sscanf(buf, "\n%d", &tmp) 如果您从不更改内部循环中的bufsscanf 将继续查看相同的输入并看到相同的数字。 是的。所以我使用另一个变量 i 来跟踪位置。但是如果缓冲区在一个数字之间停止读取(读取最后一个数字 2354 的 23),那么我就有问题了。 对。也可以处理这个问题,但这应该告诉你fread 是一个方钉,而这个问题是一个圆孔。您可以改为使用fgets() 一次读取一行。【参考方案4】:

不使用重定向时的问题是您没有导致 EOF。

由于这似乎是 Posix(基于您使用 gcc 的事实),只需键入 ctrl-D(即在按下控制按钮的同时按下/释放 d)即可到达 EOF。

如果你使用的是 Windows,我相信你会改用ctrl-Z

【讨论】:

那行得通。但我仍然遇到问题,sscanf() 只扫描第一个整数,在每个循环中 temp 的值是第一个整数。 发布了一个带有 getchar_unlocked() 的解决方案和一个分析。我可以改进它吗?【参考方案5】:

我感到困惑的一件事是,为什么您既要通过调用 setvbuf 在流对象中启用完全缓冲,又要通过将完整缓冲区读入 buf 来进行自己的缓冲。

我理解需要做缓冲,但这有点矫枉过正。

我将建议您坚持使用 setvbuf 并删除您自己的缓冲。原因是实现自己的缓冲可能很棘手。问题是当令牌(在您的情况下为数字)跨越缓冲区边界时会发生什么。例如,假设您的缓冲区是 8 个字节(尾随 NULL 总共 9 个字节),并且您的输入流看起来像

12345 12345

第一次填充你得到的缓冲区:

"12345 12"

第二次填充缓冲区时:

"345"

正确的缓冲要求您处理这种情况,因此您将缓冲区视为两个数字 12345, 12345 而不是三个数字 12345, 12, 234。

既然 stdio 已经为您处理了,就使用它。继续调用setvbuf,去掉fread,使用scanf从输入流中读取单个数字。

【讨论】:

现在你完全明白了我的问题。为了正确理解,我仍然想使用 fread :)。虽然,接下来要做的只是 setvbuf。 仅供参考,我首先尝试单独使用 setvbuf,然后我也得到了相同的执行时间(~5 秒)。我只是想加快 IO 的速度。 除非你有一个非常糟糕的 stdio 版本,否则你不会通过自己的缓冲获得任何显着的加速。 @samuel : 请看我的回答:) setvbuf 有时可能非常有效。例如,在从 SD 卡读取 45KB 数据块的情况下,将其设置为 1MB 确实有很大帮助。如果不使用它,有时读取可能需要半秒,但现在不到 0.05 秒。【参考方案6】:

好吧,从顶部开始,scanf("%d%d",&n,&k) 只会将一个值推入 n 并默默地保持 k 未设置 - 如果您检查了 scanf( ),它告诉你它填充了多少变量。我认为您希望 scanf("%d %d",&n,&k) 带有空格。

其次,n 是要运行的迭代次数,但您测试“n>0”但从不减少它。因此,n>0 始终为真,循环不会退出。

正如其他人提到的,通过管道提供标准输入会导致循环退出,因为标准输入的末尾有一个 EOF,这导致 fread() 返回 NULL,从而退出循环。您可能想在其中的某处添加“n=n-1”或“n--”。

接下来,在你的 sscanf 中, %n 并不是一个标准的东西;我不确定它是什么意思,但它可能什么都不做:scanf() 通常会在第一个无法识别的格式标识符处停止解析,这在这里什么都不做(因为您已经获得了数据),但这是不好的做法。

最后,如果性能很重要,最好不要使用 fread() 等,因为它们并不是真正的高性能。查看 isdigit(3) 和 iscntrl(3) 并考虑如何解析使用 read(2) 读取的原始数据缓冲区中的数字。

【讨论】:

scanf("%d%d",&n,&k) 没问题。 --n 实际上在那里。现在被误删了。 %n 存储读取的字符数。【参考方案7】:

我将建议尝试使用 setvbuf 进行完全缓冲并放弃 fread。如果规范是每行有一个数字,我会认为这是理所当然的,使用fgets 读取整行并将其传递给strtoul 解析应该在该行上的数字。

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

#define INITIAL_BUFFER_SIZE 2 /* for testing */

int main(void) 
    int n;
    int divisor;
    int answer = 0;
    int current_buffer_size = INITIAL_BUFFER_SIZE;
    char *line = malloc(current_buffer_size);

    if ( line == NULL ) 
        return EXIT_FAILURE;
    

    setvbuf(stdin, (char*)NULL, _IOFBF, 0);

    scanf("%d%d\n", &n, &divisor);

    while ( n > 0 ) 
        unsigned long dividend;
        char *endp;
        int offset = 0;
        while ( fgets(line + offset, current_buffer_size, stdin) ) 
            if ( line[strlen(line) - 1] == '\n' ) 
                break;
            
            else 
                int new_buffer_size = 2 * current_buffer_size;
                char *tmp = realloc(line, new_buffer_size);
                if ( tmp ) 
                    line = tmp;
                    offset = current_buffer_size - 1;
                    current_buffer_size = new_buffer_size;
                
                else 
                    break;
                
            
        
        errno = 0;
        dividend = strtoul(line, &endp, 10);
        if ( !( (endp == line) || errno ) ) 
            if ( dividend % divisor == 0 ) 
                answer += 1;
            
        
        n -= 1;
    

    printf("%d\n", answer);
    return 0;

我使用 Perl 脚本生成 0 到 1,000,000 之间的 1,000,000 个随机整数,并在我的 Windows XP 笔记本电脑上使用gcc version 3.4.5 (mingw-vista special r3) 编译此程序后检查它们是否可被 5 整除。整个过程不到 0.8 秒。

当我使用setvbuf(stdin, (char*)NULL, _IONBF, 0); 关闭缓冲时,时间增加到大约 15 秒。

【讨论】:

您能解释一下放弃fread 并转到setvbuf 的原因吗? 所以,要点是:1)没有理由去尝试消除缓冲的IO; 2)没有很好的理由说明为什么要读取二进制块并逐位解析数字。相反,依赖库的缓冲和解析。【参考方案8】:

所有这些过时的优化对运行时的影响可以忽略不计的原因是,在 *nix 和 windows 类型的操作系统中,操作系统处理文件系统的所有 I/O 并实现了 30 年的研究、诡计和诡计非常有效地做到这一点。

您试图控制的缓冲仅仅是您的程序使用的内存块。因此,速度的任何提高都将是微乎其微的(执行 1 个大的“mov”指令与 6 或 7 个较小的“mov”指令的效果)。

如果您真的想加快速度,请尝试“mmap”,它允许您直接访问文件系统缓冲区中的数据。

【讨论】:

正如思南提议的那样,加速非常重要。从大约 5 秒到 0.8 秒。你现在有什么要说的:P?【参考方案9】:

版本 1:按照 R Samuel Klatchko 的建议使用 getchar_unlocked(参见 cmets)

#define BUFSIZE 32*1024
int main()
  int lines, number=0, dividend, ans=0;
  char c;
  setvbuf(stdin, (char*)NULL, _IOFBF, 0);// full buffering mode
  scanf("%d%d\n", &lines, &dividend);
  while(lines>0)
    c = getchar_unlocked();
    //parse the number using characters
    //each number is on a separate line
    if(c=='\n')
      if(number % dividend == 0)    ans += 1;
      lines -= 1;
      number = 0;
    
    else
      number = c - '0' + 10*number;
  

  printf("%d are divisible by %d \n", ans, dividend);
  return 0;

版本 2:使用fread 读取块并从中解析数字。

#define BUFSIZE 32*1024
int main()
int lines, number=0, dividend, ans=0, i, chars_read;
char buf[BUFSIZE+1] = 0; //initialise all elements to 0
scanf("%d%d\n",&lines, &dividend);

while((chars_read = fread(buf, 1, BUFSIZE, stdin)) > 0)
  //read the chars from buf
  for(i=0; i < chars_read; i++)
    //parse the number using characters
    //each number is on a separate line
    if(buf[i] != '\n')
      number = buf[i] - '0' + 10*number;
    else
      if(number%dividend==0)    ans += 1;
      lines -= 1;
      number = 0;
           
  

if(lines==0)  break;


printf("%d are divisible by %d \n", ans, dividend);
return 0;


结果:(1000 万个数字测试可被 11 整除)

运行 1:(没有 setvbuf 的版本 1)0.782 秒 运行 2:(带有 setvbuf 的版本 1)0.684 秒 运行 3:(版本 2)0.534

附: - 每次运行都使用 GCC 编译,使用 -O1 标志

【讨论】:

解决数字可能在缓冲区末尾被截断的问题,但如果一行包含"z\n",会发生什么? 你的结论不正确。一半的加速来自进行自己的字符 -> 数字转换,而不是使用 scanf。另一半是 stdio 锁定会增加相当多的开销。试试这个:1)启用对setvbuf的调用,2)使用getchar_unlocked而不是fread逐字节读取数据。您将获得类似的加速。 @Samuel:好的。今天试试看。 @Sinan Ünür:这是对问题规范(来自 SPOJ)的解决方案,它清楚地表明每行只有 1 个数字。所以我只考虑了这一点。当然,这不是一个通用的解决方案。顺便说一句,我在我的问题中也提到过! 也不处理负数。也许您应该链接到问题规范?【参考方案10】:

这是我逐字节的看法:

/*

Buffered reading from stdin using fread in C,
http://***.com/questions/2371292/buffered-reading-from-stdin-for-performance

compile with:
gcc -Wall -O3  fread-stdin.c

create numbers.txt:
echo 1000000 5 > numbers.txt
jot -r 1000000 1 1000000 $RANDOM >> numbers.txt

time -p cat numbers.txt | ./a.out

*/

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

#define BUFSIZE 32

int main() 

   int n, k, tmp, ans=0, i=0, countNL=0;
   char *endp = 0;

   setvbuf(stdin, (char*)NULL, _IOFBF, 0);       // turn buffering mode on
   //setvbuf(stdin, (char*)NULL, _IONBF, 0);     // turn buffering mode off

   scanf("%d%d\n", &n, &k);

   char singlechar = 0;
   char intbuf[BUFSIZE + 1] = 0;

   while(fread(&singlechar, 1, 1, stdin))     // fread byte-by-byte
   
      if (singlechar == '\n') 
      
         countNL++;
         intbuf[i] = '\0';
         tmp = strtoul(intbuf, &endp, 10);
         if( tmp % k == 0) ++ans;
         i = 0;
       else 
         intbuf[i] = singlechar; 
         i++;
      
      if (countNL == n) break;
   

   printf("%d integers are divisible by %d.\n", ans, k);
   return 0;


【讨论】:

使用相同的数据,上面的代码需要 9.389 秒,而带有 -O3 标志的代码需要 0.572 秒(对于我提交的答案)。【参考方案11】:

如果您追求彻底的速度并且在 POSIX-ish 平台上工作,请考虑使用内存映射。我使用标准 I/O 对思南的回答进行了计时,并使用内存映射创建了下面的程序。请注意,如果数据源是终端或管道而不是文件,则内存映射将不起作用。

一百万个值在 0 到 10 亿之间(固定除数为 17),这两个程序的平均时间为:

标准 I/O:0.155 秒 内存映射:0.086s

大致而言,内存映射 I/O 的速度是标准 I/O 的两倍。

在忽略热身运行后,每次都重复计时 6 次。命令行是:

time fbf < data.file    # Standard I/O (full buffering)
time mmf < data.file    # Memory mapped file I/O

#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>

static const char *arg0 = "**unset**";
static void error(const char *fmt, ...)

    va_list args;
    fprintf(stderr, "%s: ", arg0);
    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    va_end(args);
    exit(EXIT_FAILURE);


static unsigned long read_integer(char *src, char **end)

    unsigned long v;
    errno = 0;
    v = strtoul(src, end, 0);
    if (v == ULONG_MAX && errno == ERANGE)
        error("integer too big for unsigned long at %.20s", src);
    if (v == 0 && errno == EINVAL)
        error("failed to convert integer at %.20s", src);
    if (**end != '\0' && !isspace((unsigned char)**end))
        error("dubious conversion at %.20s", src);
    return(v);


static void *memory_map(int fd)

    void *data;
    struct stat sb;
    if (fstat(fd, &sb) != 0)
        error("failed to fstat file descriptor %d (%d: %s)\n",
              fd, errno, strerror(errno));
    if (!S_ISREG(sb.st_mode))
        error("file descriptor %d is not a regular file (%o)\n", fd, sb.st_mode);
    data = mmap(0, sb.st_size, PROT_READ, MAP_PRIVATE, fileno(stdin), 0);
    if (data == MAP_FAILED)
        error("failed to memory map file descriptor %d (%d: %s)\n",
              fd, errno, strerror(errno));
    return(data);


int main(int argc, char **argv)

    char *data;
    char *src;
    char *end;
    unsigned long k;
    unsigned long n;
    unsigned long answer = 0;
    size_t i;

    arg0 = argv[0];
    data = memory_map(0);

    src = data;

    /* Read control data */
    n = read_integer(src, &end);
    src = end;
    k = read_integer(src, &end);
    src = end;

    for (i = 0; i < n; i++, src = end)
    
        unsigned long v = read_integer(src, &end);
        if (v % k == 0)
            answer++;
    

    printf("%lu\n", answer);
    return(0);

【讨论】:

@Jonathan Leffler:感谢您的回答:)。现在,我发布的解决方案(使用 fread)也在 0.08 秒内完成。因此,MMAPed IO 没有显着改进。我已经编辑了问题,请检查更改。

以上是关于在 C 中使用 fread 从标准输入缓冲读取的主要内容,如果未能解决你的问题,请参考以下文章

C:从标准输出块中读取

C中的管道,用于读取标准输入的缓冲区

将输入从标准输入传递到函数时进行缓冲

在linux下怎样用fread函数从输入端读取数据,输入的数据数不确定,就是从从终端想输多少就读多少那种

C语言中如何使用fread

stdin流和缓冲区