在 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
循环中调整 buf
,sscanf
会一遍又一遍地读取相同的数字。
如果您切换到使用strtol()
而不是sscanf()
,那么您可以使用endptr
输出参数在读取数字时在缓冲区中移动。
【讨论】:
您还需要更改sscanf
字符串,请参阅更新后的答案。
我现在正在使用 n>0 && sscanf(buf,"%d",&tmp),虽然它停止了,但打印的答案是错误的。而且每个数字都在不同的行,所以我猜是 sscanf(buf, "\n%d", &tmp)
如果您从不更改内部循环中的buf
,sscanf
将继续查看相同的输入并看到相同的数字。
是的。所以我使用另一个变量 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, ÷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, ÷nd);
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 从标准输入缓冲读取的主要内容,如果未能解决你的问题,请参考以下文章