Буферное чтение из stdin с использованием fread в C

Я пытаюсь эффективно читать от stdin С помощью setvbuf в режиме ' _IOFBF~. Я новичок в буферизации. Я ищу работающего примеры.

вход начинается с двух целых чисел (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%dn", &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 %dn",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 от 2354n, когда он должен был либо читать 2354 (который он не может) или вообще ничего.

как я могу решить эту проблему?


редактировать
разрешено сейчас (с анализом).

редактировать
Полная Спецификация Проблемы

11 ответов


Я собираюсь рекомендовать попробовать полную буферизацию с 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 и проверил, делятся ли они на 5 после компиляции этой программы с gcc version 3.4.5 (mingw-vista special r3) на мой Windows XP ноутбук. Все это заняло менее 0,8 секунд.

когда я отключил буферизацию с помощью setvbuf(stdin, (char*)NULL, _IONBF, 0);, срок-до 15 секунд.


одна вещь, которую я нахожу запутанной, - это то, почему вы оба включаете полную буферизацию в объекте stream через вызов setvbuf и делать свою собственную буферизацию, читая полный буфер в buf.

Я понимаю необходимость буферизации, но это немного перебор.

Я собираюсь рекомендовать вам придерживаться setvbuf и удалите свою собственную буферизацию. Причина в том, что реализация собственной буферизации может быть сложной. Проблема в том, что произойдет, когда токен (в вашем случае число) охватывает границу буфера. Например, предположим, что ваш буфер равен 8 байтам (всего 9 байтов для конечного NULL), а ваш входной поток выглядит как

12345 12345

при первом заполнении буфера вы получаете:

"12345 12"

при втором заполнении буфера вы получаете:

"345"

правильная буферизация требует, чтобы вы обрабатывали этот случай, поэтому вы рассматриваете буфер как два числа {12345, 12345}, а не три числа {12345, 12, 234}.

поскольку stdio обрабатывает это уже для вас, просто используйте это. Продолжайте звонить setvbuf, избавься от fread и использовать scanf для чтения отдельных номеров из входного потока.


Версия 1: Использование getchar_unlocked как полагает Р Самуэль Klatchko (см. комментарии)

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

результаты: (10 миллионов чисел, проверенных на делимость на 11)

выполнить 1: (Версия 1 Без setvbuf) 0.782 сек
Выполнить 2: (Версия 1 с setvbuf) 0.684 сек
Выполнить 3: (Версия 2 ) 0.534

С. П. - Каждый запуск скомпилирован с помощью GCC с использованием-O1 flag


проблема, когда вы не используете перенаправление, заключается в том, что вы не вызываете EOF.

поскольку это, по-видимому, Posix (на основе того, что вы используете gcc), просто введите ctrl-D (т. е. при нажатии кнопки управления нажмите / отпустите d), что приведет к достижению EOF.

Если вы используете Windows, я считаю, что вы используете ctrl-Z вместо.


Если вы находитесь после out-and-out speed и работаете на платформе POSIX-ish, рассмотрите возможность использования сопоставления памяти. Я взял ответ Синана с помощью стандартного ввода-вывода и синхронизировал его, а также создал программу ниже, используя отображение памяти. Обратите внимание, что сопоставление памяти не будет работать, если источником данных является терминал или канал, а не файл.

с миллионом значений от 0 до миллиарда (и фиксированным делителем 17), средние тайминги для двух программ было:

  • стандартный I / O: 0.155 s
  • карта памяти: 0.086 s

ориентировочно, памяти ввода/вывода Как стандартный ввод-вывод.

в каждом случае время повторялось 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 != '' && !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;
}

проблема, которую вы продолжаете иметь, заключается в том, что вы никогда не настраиваете buf внутренний while цикл sscanf продолжает читать один и тот же номер снова и снова.

если вы переключаетесь на использование strtol() intead sscanf(), вы можете использовать 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 для получения строки данных неизвестной длины из потока.)


причина, по которой вся эта оптимизация permature оказывает незначительное влияние на время выполнения, заключается в том, что в операционных системах типа *nix и windows ОС обрабатывает все входы и выходы из файловой системы и реализует 30 лет исследований, обмана и коварства, чтобы сделать это очень эффективно.

буферизация вы пытаетесь контролировать-это просто блок памяти, используемых программой. Таким образом, любое увеличение скорости будет минимальным (эффект выполнения 1 больших " mov " стихов 6 или 7 меньше 'mov' instructions).

Если вы действительно хотите ускорить это, попробуйте "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 <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] = '';
         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;

}