使用fread()/fgets()而不是fgetc()逐行读取变长文本文件(块I/O vs.字符I/O)

6
有没有使用fread(块I/O)而不是fgetc(字符I/O)的getline函数?
通过fgetc逐个字符地读取文件会导致性能下降。我们认为,为了提高性能,可以在getline的内部循环中使用fread进行块读取。然而,这会产生潜在的不良影响,即可能会超出一行的末尾。至少,这将要求实现getline来跟踪文件的“未读”部分,这需要超出ANSI C文件语义的抽象。这不是我们想自己实现的东西!
我们已经对应用程序进行了分析,发现性能缓慢的原因是通过fgetc逐个字符地消耗大文件。相比之下,其他开销的成本实际上微不足道。我们总是按顺序从头到尾顺序阅读文件的每一行,并且可以在读取期间锁定整个文件。这可能使基于freadgetline更容易实现。
那么,是否存在使用fread(块I/O)而不是fgetc(字符I/O)的getline函数?我们相当确定它存在,但如果不存在,我们应该如何实现它?
更新:发现了一篇有用的文章,由Paul Hsieh撰写,名为在C中处理用户输入。它是基于fgetc的方法,但讨论了其他替代方案(从多么糟糕的gets开始,然后讨论fgets)。
另一方面,C程序员(即使是经验丰富的程序员)通常会反驳说应该使用 fgets() 作为替代方法。当然,单独使用 fgets() 并不能真正处理用户输入。除了具有奇怪的字符串终止条件(遇到 \n 或 EOF 而不是 \0),当缓冲区达到容量时所选择的终止机制是简单地中断 fgets() 操作并以 \0 终止它。因此,如果用户输入超过了预分配缓冲区的长度,则 fgets() 返回部分结果。为了处理这个问题,程序员有几种选择:1)只需处理被截断的用户输入(在提供输入时无法向用户反馈输入已被截断),2)模拟可增长的字符数组,并使用连续调用 fgets() 填充它。第一个解决方案对于变长用户输入来说几乎总是一个非常糟糕的解决方案,因为大多数情况下缓冲区不可避免地会太大,因为它试图捕获太多普通情况,而对于不寻常的情况则太小。第二个解决方案不错,但正确实现可能比较复杂。两种解决方案都无法处理 fgets() 关于 '\0' 的奇怪行为。

留给读者的练习:为了确定调用 fgets() 时实际读取了多少字节,可以尝试像它一样扫描 '\n' 并跳过任何 '\0',同时不超过传递给 fgets() 的大小。解释为什么这对于流的最后一行是不足够的。ftell() 的哪个缺陷阻止了它完全解决这个问题?

留给读者的练习:通过在每次调用 fgets() 之间使用非零值覆盖整个缓冲区来解决确定由 fgets() 消耗的数据长度的问题。

因此,使用 fgets(),我们只能选择编写大量代码并接受与 C 库其余部分不一致的行终止条件,或者有一个任意的截止点。如果这还不够好,那我们还剩下什么? scanf() 将解析和读取混合在一起,无法分开,而 fread() 会读取字符串的末尾。简而言之,C 库没有留下任何东西。我们被迫在 fgetc() 的基础上自己编写代码。那么让我们试一试。

因此,是否存在基于 fgets(并且不截断输入)的 getline 函数?


关于你在最后提出的新问题,是的,它存在。我在我的答案中概述了它。你引用的文章提到了一个最终非换行符结尾行的问题;我通过使用 '\n' 预填充缓冲区并提供一种检测条件的方法来解决了这个问题。 - R.. GitHub STOP HELPING ICE
1
另外请注意,Paul Hsieh的解决方案使用fgetc非常糟糕。在现代实现中,由于需要支持锁定,以防止多个线程访问同一个FILE对象,使用fgetc将非常缓慢。您可以使用getc_unlocked(但这是一个POSIX函数,而不是标准C函数),但即使使用最佳的宏展开getc_unlockedfgets搜索缓冲区\n(即使用memchr)的方式也比没有访问内部缓冲区时快得多。还要注意,如果您有POSIX(2008),那么已经有了getline - R.. GitHub STOP HELPING ICE
2个回答

5
不要使用fread,使用fgets。我猜这是一道作业/课程项目问题,所以我不会提供完整的答案,但如果你说不是,我会给出更多建议。绝对可以通过纯粹使用fgets来提供GNU风格getline的100%语义,包括嵌入的空字节,但需要一些巧妙的思考。
好的,更新一下,因为这不是作业:
  • 将缓冲区的memset设置为'\n'
  • 使用fgets
  • 使用memchr查找第一个'\n'
  • 如果没有找到'\n',则该行超过了您的缓冲区。扩大缓冲区,用'\n'填充新部分,并在新部分中使用fgets,如有必要重复此步骤。
  • 如果'\n'后面的字符是'\0',则fgets由于到达行末而终止。
  • 否则,fgets由于到达EOF而终止,'\n'留在memset中,前一个字符是fgets写入的终止空字符,前面的字符是实际读取数据的最后一个字符。
如果您不关心支持带有嵌入空字符的行,则可以消除memset并将memchr替换为strlen(无论哪种方式,空字符都不会终止读取;它只是您读入行的一部分)。
还有一种使用fscanf"%123[^\n]"指示符(其中123是您的缓冲区限制),可以让您停在非换行符上的方法(类似于GNU getdelim)。但是,除非你的系统有非常高级的scanf实现,否则它可能很慢。

这不是作业... :) 你会如何建议使用 fgets?使用可增长的字符数组,并通过连续调用 fgets 填充它似乎很难正确实现。此外,我了解到 fgets 在遇到 '\n' 或 EOF 时终止,但不是 '\0'。虽然这对我们的文件没有问题。 - Julienne Goldberg
1
@R.. 一个小漏洞:在使用 char s[5]; memset(s, '\n', sizeof s); fgets(s, sizeof s, ...); 处理包含3个字节“xyz”的文件时,会导致s中出现“xyz\0\n”。找到第一个 '\n' 是可以的,但检查后面的字符是未定义行为。建议添加“如果最后一个位置是 '\n',则 fgets 终止是因为到达了文件的最后一行。” 然后继续说:“如果接下来的字符…” - chux - Reinstate Monica
我想知道为什么这么多与字符串相关的函数具有相对无用的返回值?调用strcatfgets的代码通常需要找到最后一个写入的字符--这些函数的代码已经知道了。我想不出这些函数实现中返回值的任何用处。 - supercat

1

fgets和fgetc/setvbuf之间的性能差异不大。 尝试:

int c;
FILE *f = fopen("blah.txt","r");
setvbuf(f,NULL,_IOLBF,4096); /* !!! check other values for last parameter in your OS */
while( (c=fgetc(f))!=EOF )
{
  if( c=='\n' )
    ...
  else
    ...
} 

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