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

时间:2010-03-03 12:48:23

标签: c buffering fread

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

输入以两个整数(nk)开头。下一行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将从23读取2354\n,当它应该是{{1} (它不能)或根本没有。

我该如何解决这个问题?


编辑
Resolved now (with analysis)

编辑
Complete Problem Specification

11 个答案:

答案 0 :(得分:3)

我建议您使用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脚本生成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);关闭缓冲时,时间上升到大约15秒。

答案 1 :(得分:2)

我觉得有些困惑的一点是,为什么你要通过调用setvbuf在流对象中启用完全缓冲,并通过将完整缓冲区读入buf来进行自己的缓冲。

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

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

12345 12345

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

"12345 12"

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

"345"

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

由于stdio已经为您处理,只需使用它。继续致电setvbuf,摆脱fread并使用scanf从输入流中读取个别数字。

答案 2 :(得分:2)

版本1:按照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

P.S。 - 每次使用-O1标志用GCC编译的运行

答案 3 :(得分:1)

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

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

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

答案 4 :(得分:1)

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

有一百万个值在0到十亿之间(固定除数为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 <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);
}

答案 5 :(得分:0)

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

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

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

并将内部的主体更改为:

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

您继续遇到的问题是因为您从未在内部buf循环中调整whilesscanf会一遍又一遍地读取相同的数字。

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

答案 6 :(得分:0)

好吧,从顶部开始,scanf(“%d%d”,&amp; n,&amp; k)只会将一个值推入n并静默地保持k未设置 - 如果你检查了返回,你会看到这个scanf()的值,它告诉你它填充了多少变量。我想你想要scanf(“%d%d”,&amp; n,&amp; k)和空格。

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

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

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

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

答案 7 :(得分:-1)

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

答案 8 :(得分:-1)

Mabe还看一下这个getline实现:

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

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

答案 9 :(得分:-2)

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

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

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

答案 10 :(得分:-2)

这是我对逐字节的看法:

/*

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 <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;

}