C语言中的更快I/O操作

8

我有一个问题需要从控制台输入像下面这样的1000000行输入。

0 1 23 4 5
1 3 5 2 56
12 2 3 33 5
...
...

我曾经使用过scanf,但是它非常慢。有没有一种更快的方式从控制台获取输入?我可以使用read(),但是我不确定每行有多少字节,所以我不能要求read()读取'n'字节。 谢谢, 非常感谢


你尝试过使用readline吗? - ctrl-alt-delor
8个回答

7
使用fgets(...)逐行读取。请注意,您应该检查行末的'\n',如果没有,则表示您已经到达EOF,或者需要读取另一个缓冲区的内容,并将两个缓冲区连接起来。反复执行此操作以防止缓冲区溢出。
接下来,您可以自己在内存中解析每一行。我喜欢使用strspn(...)和strcspn(...)进行解析,但您可能会有不同的方法。
解析: 定义一个分隔符字符串。使用strspn()计算匹配分隔符的“非数据”字符数,并跳过它们。使用strcspn()计算不匹配分隔符的“数据”字符数。如果此计数为0,则表示该行没有更多数据。否则,将这N个字符复制出来,交给像atoi(...)或sscanf(...)这样的解析函数。然后,将指针基础重置为此块的末尾,并重复跳过分隔符、复制数据、转换为数字的过程。

4
如果您的示例是代表性的,确实每行有五个小数位的固定格式,我可能会使用fgets()组合来读取行,然后使用循环调用strtol()将字符串转换为整数。这样应该比scanf()更快,同时仍然比自己进行字符串到整数转换更清晰和更高级。类似这样:
typedef struct {
  int number[5];
} LineOfNumbers;

int getNumbers(FILE *in, LineOfNumbers *line)
{
  char buf[128];  /* Should be large enough. */
  if(fgets(buf, sizeof buf, in) != NULL)
  {
    int i;
    char *ptr, *eptr;

    ptr = buf;
    for(i = 0; i < sizeof line->number / sizeof *line->number; i++)
    {
      line->number[i] = (int) strtol(ptr, &eptr, 10);
      if(eptr == ptr)
        return 0;
      ptr = eptr;
    }
    return 1;
  }
  return 0;
}

注意:这是未经测试(甚至未编译!)的浏览器编写的代码。但或许能作为具体示例有所帮助。

我喜欢 "char buf[128]; /* 应该足够大。*/" 这是一个很好的练习。 - Chris Lutz
@Chris:嗯...不确定你是否在讽刺。这是一个例子代码,基于一个非常模糊的规格说明,并且fgets()也使用了sizeof,所以这里应该没有溢出的风险。或者我只是过于多疑了。 - unwind
1
我是在讽刺。我倾向于避免固定长度的行读取,即使它可以正常工作,只是因为我通常喜欢将整行一次性存储在一个变量中(这可能是我从Perl继承下来的口味)。但这段代码很好。即使这些评论是真实的,我也觉得看到它们很有趣。 - Chris Lutz

3
你可以使用多个具有固定大小缓冲区的read,直到读取到文件结尾。

2

出于好奇,什么会在控制台中生成如此快速且数量众多的行?


1
% a.out < big.file 类似这样的东西,我猜测。 - Roboprog
如果这是某个程序的标准输出,将其重定向到文件并从文件中加载会更快。控制台渲染是一个缓慢的过程。 - Indy9000

2
如果可以的话,请使用二进制I/O。文本转换可能会使读取速度变慢几倍。如果您之所以使用文本I/O是因为易于调试,请再次考虑二进制格式,并在需要时使用od程序(假设您正在使用unix)使其可读。
还有一件事:有AT&T的SFIO库,它代表更安全/更快的文件IO。您也可能会有一些运气,但我怀疑您不会获得与二进制格式相同的加速。

使用二进制文件的Unix程序并不是真正的Unix程序,或者类似这样的东西 :-)在实际操作中尽可能使用人类可读的数据 -- 或者 -- 老板/老师说这就是我们得到的! - Roboprog
1
++ 如果有选择的话,二进制是最快的。如果列表中有非整数,二进制也是最精确的。 - Mike Dunlavey

1

逐行读取(如果缓冲区不足以容纳一行,则扩展并继续使用更大的缓冲区)。

然后使用专用函数(例如atoi)而不是通用函数进行转换。

但最重要的是,建立一个可重复的测试框架,并进行分析以确保改进真正加速了事情。


0

通过使用fread()fread_unlocked()(如果您的程序是单线程的)来获取输入,可以大大减少执行时间。锁定/解锁输入流只需要一次,所需时间微不足道,因此请忽略它。

以下是代码:

#include <iostream>

int maxio=1000000;
char buf[maxio], *s = buf + maxio;

inline char getc1(void)
{
   if(s >= buf + maxio) { fread_unlocked(buf,sizeof(char),maxio,stdin); s = buf; }
   return *(s++);
}
inline int input()
{
   char t = getc1();
   int n=1,res=0;
   while(t!='-' && !isdigit(t)) t=getc1(); if(t=='-')
   {
      n=-1; t=getc1();
   }
   while(isdigit(t))
   {
     res = 10*res + (t&15);
     t=getc1();
   }
   return res*n;
}

这个用 C++ 实现。在 C 中,你不需要包含 iostream,函数 isdigit() 隐式可用。

你可以通过调用 getc1() 来获取字符流,并通过调用 input() 获取整数输入。

使用 fread() 的整个想法是要一次性获取所有输入。重复调用 scanf()/printf() 会占用大量时间来锁定和解锁流,在单线程程序中完全是多余的。

还要确保 maxio 的值足以在几个“往返”中获取所有输入(理想情况下只有一个往返)。必要时进行调整。

希望能对你有所帮助!


0

fread 如果试图读取的字节超过文件中实际存在的字节数,仍将返回。

我发现读取文件最快的方法之一是这样的:

/* 定位到文件末尾 */ fseek(file,0,SEEK_END);

/* 获取文件大小 */ size = ftell(file);

/* 定位到文件开头 */ fseek(file,0,SEEK_SET);

/* 为文件创建缓冲区 */ buffer = malloc(1048576);

/* 每次读入1MB,直到达到size字节等 */

在现代计算机上,利用内存并将整个文件加载到内存中,然后可以轻松地遍历内存。

至少你应该使用fread,并且块大小尽可能大,至少与缓存块或HDD扇区大小一样(最小为4096字节,我个人会使用1048576作为最小值)。你会发现,使用更大的读取请求,fread能够顺序地在一次操作中获取一个大流。这里有些人建议使用128字节是荒谬的...因为你最终会发现驱动器不断寻找,因为调用之间的微小延迟将导致磁头已经超过了下一个几乎肯定包含你想要的顺序数据的扇区。


你可以使用malloc()函数一次性分配足够大的缓冲区来容纳所有size,但这会消耗很多内存,我理解为什么要避免这种情况。 - Chris Lutz
在管道中,EOF定位无效。stdin通常只是重新定向的管道,而不是实际的文件。我认为“控制台”指的是stdin。就我所知,这似乎是Windows语言的一小部分遗留问题。 - Roboprog
提到Windows:这个解决方案涉及使用内存映射文件。 - Roboprog
如果您的操作系统(OS)有一个像Linux这样的现代内核,请不要复制。内核将使用您拥有的所有RAM进行缓存。如果您复制到RAM(malloc缓冲区),则RAM中将有两个副本,除非您因为RAM不足而无法实现此操作,此时缓存将被刷新且malloc的内存将会被交换。 - ctrl-alt-delor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接