使用C中的fread从stdin缓冲读取
我试图通过在`_IOFBF~模式中使用setvbuf
来有效地读取stdin
。 我是新来的缓冲。 我正在寻找工作实例。
输入以两个整数( n
, k
)开头。 接下来的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, ÷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
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; }