Linux 管道到底有多快?

这篇文章主要探讨了在 Linux 中 Unix 管道的实现方式,通过逐步优化一个通过管道读写数据的测试程序,将其吞吐量从约 3.5GiB/s 提高了 20 倍。

  • 挑战与初始版本:测量了传说中的 FizzBuzz 程序的性能,其输出速率为 36GiB/s。然后实现了自己的管道读写程序,使用writeread系统调用,每次写入 256KiB 的缓冲区,速度仅为 FizzBuzz 的 1/10,约 3.7GiB/s。
  • write的问题:使用perf工具分析程序时间花费,发现pipe_write函数中约 47%的时间用于复制或分配页面。Linux 管道是一个环形缓冲区,head存储写端,tail存储读端。pipe_write工作时,如果管道已满则等待空间,先填充当前head指向的缓冲区,然后分配新页面并填充,整个过程受锁保护。由于需要多次复制页面、处理非连续内存、获取和释放管道锁等,导致速度比单线程顺序 RAM 读取慢约 4 倍。
  • 拼接的救援:介绍了vmsplicesplice系统调用,它们可以在不复制的情况下将数据移动到和从管道中,从而提高性能。通过将write替换为vmsplice,并使用双缓冲方案,吞吐量提高了三倍多,达到 12.7GiB/s。将read也改为使用splice,消除了所有复制操作,速度又提高了 2.5 倍,达到 32.8GiB/s。
  • 寻找页面:使用perf分析vmsplice的性能,发现大部分时间花费在锁定管道和移动页面上。介绍了 Linux 的分页机制,进程使用虚拟内存地址,通过页表映射到物理内存地址,内存被分割成均匀大小的页面(如 x86-64 上的 4KiB)。struct page数据结构用于表示物理页面,iov_iter_get_pages函数将struct iovec转换为struct pageget_user_pages_fast函数通过遍历页表将虚拟页面转换为物理页面。使用 2MiB 巨大页面可以提高性能约 50%,虽然struct page仍指向 4KiB 页面,但巨大页面的簿记和地址转换操作更便宜。
  • 忙碌循环:发现程序花费大量时间等待管道可写和唤醒等待管道有内容的读者,通过让vmsplicesplice在不可写时返回并忙碌循环,性能又提高了 25%,达到 62.5GiB/s。但忙碌循环会占用 CPU 核心。
  • 总结与思考:系统地通过查看perf输出和 Linux 源代码提高了程序性能,涉及零拷贝操作、环形缓冲区、分页和虚拟内存、同步开销等主题。还提到了一些未详细讨论的细节和有趣的话题,如缓冲区分配、进程绑定、其他测试选项、get_user_pages_fast的合成基准测试以及拼接的潜在危险等。

总的来说,通过对 Linux 管道实现的深入研究和优化,实现了显著的性能提升。

阅读 28
0 条评论