使用C中的fread从stdin缓冲读取

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

输入以两个整数( nk )开头。 接下来的n行输入包含1个整数。 目的是打印可以被k整除的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 (它不能)或者根本不读取。

我该如何解决这个问题?


编辑
现已解决(通过分析) 。

编辑
完整的问题规范

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

 #include  #include  #include  #include  #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脚本生成1,000,000个0到1,000,000之间的随机整数,并在我的Windows XP笔记本电脑上使用gcc version 3.4.5 (mingw-vista special r3)编译该程序后检查它们是否可被5整除。 整件事花了不到0.8秒。

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

我发现令人困惑的一件事是你为什么要通过调用setvbuf在流对象中启用完全缓冲,并通过将完整缓冲区读入buf来进行自己的缓冲。

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

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

 12345 12345 

第一次填充缓冲区时,您会得到:

 "12345 12" 

而第二次填充缓冲区时,你得到:

 "345" 

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

由于stdio处理已经适合你的,只需使用它。 继续调用setvbuf ,摆脱fread并使用scanf从输入流中读取单个数字。

版本1:使用R Samuel Klatchko建议的getchar_unlocked(参见评论)

 #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, ÷nd); 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

PS - 使用-O1标志使用GCC编译的每次运行

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

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

如果您使用的是Windows,我相信您会使用ctrl-Z代替。

如果您在完成速度并且在POSIX-ish平台上工作,请考虑使用内存映射。 我使用标准I / O获取了Sinan的答案并定时,并使用内存映射创建了下面的程序。 请注意,如果数据源是终端或管道而不是文件,则内存映射将不起作用。

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

  • 标准I / O:0.155s
  • 内存映射: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  #include  #include  #include  #include  #include  #include  #include  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); } 

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

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

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

并将内部的身体改为:

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

你继续遇到的问题是,因为你永远不会在内部while循环中调整buf ,所以sscanf会一遍又一遍地读取相同的数字。

如果切换到使用sscanf() strtol() endptr ,则可以使用endptr输出参数在读取数字时移动缓冲区。

好吧,从顶部开始,scanf(“%d%d”,&n,&k)将仅将值推入n并静默保留k unset – 如果检查了scanf()的返回值,您会看到这个,告诉你它填充了多少变量。 我想你想要scanf(“%d%d”,&n,&k)和空格。

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

正如其他人提到的那样,在管道上输入stdin会导致循环退出,因为stdin的结尾有一个EOF,导致fread()返回NULL,退出循环。 你可能想在那里的某处添加一个“n = n-1”或“n–”。

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

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

最外面的while()循环只有在从stdin读取时返回EOF时才会退出。 这只能在到达输入文件的实际文件结束时,或者如果写入输入管道的进程退出时才会发生。 因此,从不执行printf()语句。 我不认为这与调用setvbuf()有任何关系。

Mabe还看一下这个getline实现:

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

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

所有这些过早优化对运行时具有可忽略影响的原因是,在* nix和Windows类型的操作系统中,OS处理进出文件系统的所有I / O,并实现了30年的研究,欺骗和狡猾的做法非常有效率。

您试图控制的缓冲仅仅是程序使用的内存块。 所以速度的任何增加都是最小的(做1个大’mov’对6或7个较小的’mov’指令的效果)。

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

这是我对逐字节的看法:

 /* Buffered reading from stdin using fread in C, http://stackoverflow.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  #include  #include  #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; }