2

我想一次读取和处理(例如打印)CSV 文件第一行中的条目。我假设 Unix 风格的\n换行符,没有条目长于 255 个字符,并且(目前)在 EOF 之前有一个换行符。这意味着是一个更有效的替代fgets()方法strtok()

#include <stdio.h>
#include <string.h>

int main() {
    int i;
    char ch, buf[256];
    FILE *fp = fopen("test.csv", "r");

    for (;;) {
        for (i = 0; ; i++) {
            ch = fgetc(fp);
            if (ch == ',') {
                buf[i] = '\0'; 
                puts(buf);
                break;
            } else if (ch == '\n') {
                buf[i] = '\0'; 
                puts(buf);
                fclose(fp);
                return 0;
            } else buf[i] = ch;
        }
    }
}
  1. 这种方法是否尽可能有效和正确?
  2. 使用此方法测试 EOF 和文件读取错误的最佳方法是什么?(可能性:针对字符宏EOFfeof()ferror()等进行测试)。
  3. 我可以使用 C++ 文件 I/O 执行相同的任务而不会降低效率吗?
4

3 回答 3

5

最有效的方法在很大程度上取决于操作系统、标准库(例如libc),甚至是您运行的硬件。这使得几乎不可能告诉你什么是“最有效的”。

话虽如此,您可以尝试以下几件事:

  • 使用mmap()或等效的本地操作系统(Windows 有CreateFileMapping / OpenFileMapping / MapViewOfFile,可能还有其他)。然后你不做显式的文件读取:你只需访问文件,就好像它已经在内存中一样,任何不存在的东西都会被页面错误机制出错。
  • 手动将整个文件读入缓冲区,然后在该缓冲区上工作。调用文件读取函数的次数越少,所花费的函数调用开销就越少,并且应用程序/操作系统域切换也可能越少。显然这会使用更多内存,但可能非常值得。
  • 针对您的问题和平台使用更优化的字符串扫描器。自己一个字一个字地去几乎永远不会像依赖与你的问题领域很接近的现有东西一样快。例如,您可以打赌,strchr并且memchr可能比您可以自己滚动的大多数代码进行更好的优化,执行诸如一次读取整个缓存行或单词,使用更好的算法进行此类搜索等操作。对于更复杂的情况,您可能会考虑一个完整的正则表达式引擎,它可以将您的正则表达式编译为针对您复杂情况的快速表达式。
  • 避免复制你的字符串。考虑“查找分隔符”然后“在分隔符之间输出”可能会有所帮助。例如,您可以使用strchr查找下一个感兴趣的字符,然后fwrite直接从输入缓冲区写入标准输出。然后,您将大部分工作保存在几个本地寄存器中,而不是使用 stack 或 heap buf

但是,如果有疑问,请尝试一些可能性和配置文件,配置文件,配置文件。

同样对于此类问题,请非常注意由操作系统和硬件缓存引起的运行之间的差异:分析一堆运行,而不是每次更改后仅运行一次——如果可能,使用可能总是命中缓存的测试(如果您尝试测量最佳情况下的性能)或可能会错过的测试(如果您尝试测量最坏情况下的性能)。


关于 C++ 文件 IO(fstream等等),请注意它们是更大、更复杂的野兽。它们倾向于包括诸如语言环境管理、自动缓冲等之类的东西——以及不太容易出现特定类型的编码错误。

如果您正在做一些非常简单的事情(就像您在此处描述的那样),我倾向于发现 C++ 库的东西会妨碍您。(有时通过 stringstream 方法使用调试器和“步进指令”而不是一些 C 字符串函数,您会很快对此有很好的感觉。)

这完全取决于您将来是否想要或需要额外的功能或安全性。


最后,强制性的“不要为小事出汗”。如果它真的很重要,只花时间在这里优化。否则,请相信库和操作系统在大多数情况下会为您做正确的事情——如果您在微优化方面走得太远,您会发现您稍后会自找麻烦。这并不是要阻止您思考“我是否应该提前阅读整个文件,这会破坏未来的用例”——因为那是宏观的,而不是微观的。

但一般来说,如果你没有进行这种“让它更快”的调查是有充分理由的——即“现在我已经编写了这个应用程序,需要让它表现得更好,而这段代码在分析器中显示为很慢” ,或者“为了好玩而这样做,以便我能更好地理解系统”——好吧,先把你的时间花在其他地方。=)

于 2013-05-04T20:31:32.407 回答
3

如果您要连续扫描文件,一种方法是使用 2 个足够大的缓冲区(16K 是 SSD 的最佳大小,而 HDD IIRC 的最佳大小是 4K。但 16K 应该就足够了)。您从执行异步加载开始(在 Windows 中查找Overlapped I/O,在 Unix/OSX 上使用O_NONBLOCK) 的第一个 16K 到缓冲区 0,然后开始另一个加载到缓冲区 1 的字节 16K 到 32K。当您的读取位置达到 16K 时,交换缓冲区(因此您现在正在从缓冲区 1 读取)等待任何进一步的加载完成到缓冲区 1 中,然后将 32K 到 48K 的字节异步加载到缓冲区 0 中,依此类推。这样,您不必等待加载完成的机会就会大大减少,因为它应该在您处理之前的 16K 时发生。

我在之前使用 fopen 和 fgetc 的 XML 解析器中转移到了这样的方案,并且速度提升很大。加载一个 15 兆的 XML 文件并对其进行处理从几分钟缩短到几秒钟。当然,您的里程可能会有所不同。

于 2013-05-04T20:39:57.193 回答
0

用于fgets一次读取一行。C++ 文件 I/O 基本上是包装代码,其中包含一些编译器优化(以及许多不需要的功能)。除非您正在阅读数百万行代码并测量时间,否则这无关紧要。

于 2013-05-04T20:13:35.933 回答