【问题标题】:"Optimal" IO Buffering - Programmer's or Kernel's Task?“最佳” IO 缓冲——程序员的任务还是内核的任务?
【发布时间】:2012-12-09 19:11:09
【问题描述】:

我的任务很简单:在 Linux 上用 C++ 读取和解析一个大文件。有两种方式:

  1. 逐字节解析。

    while(/*...*/) {
            ... = fgetc(...);
            /* do something with the char */
    }
    
  2. 按缓冲区解析缓冲区。

    while(/*...*/) {
            char buffer[SOME_LARGE_NUMBER];
            fread(buffer, SOME_LARGE_NUMBER, 1, ...);
            /* parse the buffer */
    }
    

现在,逐字节解析对我来说更容易(不检查​​缓冲区有多满等)。不过听说大篇幅阅读效率更高。

什么是哲学? “最佳”缓冲内核的任务,所以当我调用fgetc() 时它已经被缓冲了?还是建议我处理它以获得最佳效率?

此外,除了所有哲学:Linux 上的现实是什么?

【问题讨论】:

  • 你也可以使用mmap(),把阅读等的工作留给内核,不用管这个。
  • @fge: 除了mmap 仅适用于本地文件,不适用于远程文件系统上的远程文件。
  • 顺便说一句,getc 可能比fgetc 更有效。后者会导致函数调用开销。
  • "程序员的任务还是内核的任务?" - 好吧,内核程序员。

标签: c++ performance io kernel buffer


【解决方案1】:

不管fgetc() 的性能或底层缓冲如何,为您需要的每个字节调用一个函数,而不是拥有一个合适大小的缓冲区来迭代,都是内核无法帮助您解决的开销。

我为我的本地系统(显然是 YMMV)做了一些快速而肮脏的计时。

我选择了一个约 200k 的文件,并对每个字节求和。我这样做了 20000 次,在使用 fgetc() 读取和使用 fread() 读取之间每 1000 个循环交替。我将每 1000 个循环计时为一个块。我编译了一个发布版本,并开启了优化。

fgetc() 循环变体始终比 fread() 循环慢 45 倍

在 cmets 中提示后,我还比较了getc(),并更改了 stdio 缓冲区。性能没有明显变化。

【讨论】:

  • 如果fgetc() 在这个“迭代”中并且调用速度很快(可能是内联的),那么应该几乎没有区别,对吧?我的意思是:如果 c=fgetc(...) 几乎和 c=buffer[i] 一样快 - 那么我们很酷,不是吗?
  • 这可能是一两个过分的假设。
  • 哇,现在你的运行时测试是一个沉重的论据!谢谢。你介意粘贴代码吗,这样我就可以看看我的 g++ 优化得如何了?
  • 也可能很有趣:你使用了哪个缓冲区大小?
  • @Johannes 尝试使用默认值(本地 512)、64k 和 200k。没有明显区别。
【解决方案2】:

stdio 缓冲区不是内核的一部分。它是用户空间的一部分。

但是,您可以使用setbuf 影响该缓冲区的大小。当缓冲区不够满时,stdio 库将通过发出 read 系统函数来填充它。

因此,使用 fgetcfread 在内核和用户之间切换并不重要。

【讨论】:

  • 有趣!你说我可以毫无顾虑地使用fgetc()。但是,我需要在这里找到setbuf() 的良好价值吗?内核不应该知道吗? :)
  • @Johannes - 这与内核无关。 Stdio 是用户空间中的一个库,用于从内核执行缓冲(因此减少了上下文切换)。至于缓冲区的大小 - 这取决于数据的性质。
【解决方案3】:

没关系,真的。即使是 SSD,I/O 开销也使花在缓冲上的时间相形见绌。当然,现在是微秒而不是毫秒,但函数调用以纳秒为单位。

【讨论】:

    【解决方案4】:

    fgetc慢的原因不是函数调用量,而是系统调用量。 fgetc 通常实现为 int fgetc(FILE *fp) { int ch; return (fread(&ch,1,1,fp)==1?ch:-1); }

    尽管 fread 本身可能缓冲 64k 或 1k,但系统调用开销与 e.g. 相比有所不同

     int fgetc_buffered(FILE *fp) {
         static int head=0,tail=0; 
         static unsigned char buffer[1024];
         if (head>tail) return buffer[tail++];
         tail=0;head=fread(buffer,1,1024,fp);
         if (head<=0) return -1;
         return buffer[tail++];
     }
    

    【讨论】:

      【解决方案5】:

      stdio 例程执行用户空间缓冲。当您调用 getc、fgetc、fread 时,它们会从 stdio 用户空间缓冲区获取数据。当缓冲区为空时,stdio 将使用内核读取调用来获取更多数据。

      设计文件系统的人都知道磁盘访问(主要是寻道)非常昂贵。因此,即使 stdio 使用 512 字节的块大小,文件系统也可能使用 4 KB 的块大小,内核一次读取文件 4 KB。

      通常内核在读取后会发起磁盘/网络请求。对于磁盘,如果它看到您按顺序读取文件,它将开始提前读取(在您请求之前获取块),以便更快地获得数据。

      内核也会在内存中缓存文件。因此,如果您正在读取的文件适合内存,则在您的程序运行一次后,该文件将保留在内存中,直到内核决定最好缓存您正在引用的其他一些文件。

      使用 mmap 不会获得内核预读的好处。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2017-02-28
        • 1970-01-01
        • 1970-01-01
        • 2019-09-05
        • 1970-01-01
        • 1970-01-01
        • 2021-09-19
        相关资源
        最近更新 更多