调用 POSIX 指定函数与直接 Linux 内核调用的性能

调用 POSIX 指定函数与直接 Linux 内核调用的性能

在一个在 Stack Overflow 上回答,我提供了一个代码示例来执行问题中引用的一些小任务。最初的问题与最快执行的技术有关(因此性能标准在这里发挥作用)。

另一位评论者/回答者建议进行 POSIX 定义的系统API 调用(在本例中为readdir)不如进行直接的系统调用到内核 ( syscall(SYS_getdents,...)) 并声称性能差异在 25% 范围内。 (我没有实施和重新基准测试;我相信性能实际上可以更好。)

我的问题是关于所提出的基于系统调用的解决方案的性能特征和为什么他们可能会更快。我可以想到性能可能更好的几个原因:

  1. POSIXreaddir本质上比syscall(SYS_getdents,...)/更复杂getdents()
  2. readdir(大概调用syscall(SYS_getdents,...)只是增加了间接的开销
  3. readdir仅返回一条记录(每个内核调用),而syscall(SYS_getdents,...)/getdents()` 每个内核调用返回(大概)多个记录

我无法想象上面的#1 是真的。readdirgetdents非常相似,以至于 glibc 中的实现readdir根本无法比直接调用syscall(SYS_getdents,...)/getdents()调用更多的“真实”系统调用。

我也无法想象 #2 是真的,因为调用readdir可能的换行getdents以及也syscall(SYS_getdents,...)可能的调用getdents(建议的答案专门使用syscall(SYS_getdents,...)而不是getdents直接调用。Linux 上 glibc 中的所有内容都可能归结为syscall(syscallid, args)在这种情况下#2大概真的。

在我看来,最后一种可能性是最好的解释:更少的内核调用只会带来更快的性能。

对于为什么“直接内核调用”比调用 POSIX 定义的函数要快得多,是否有任何具体解释?

答案1

考虑到这是 Linux 中最昂贵的调用之一,诸如PLT间接或syscall()'s可变参数(寄存器必须保存到内存中)之类的因素应该不起什么作用。getdents

在我的计算机上完全读取一个空目录大约需要 5µs,读取一个包含 100 个项目的目录需要 37µs,读取一个包含 1000 个项目的目录需要 340µs,读取一个包含 10,000 个项目的目录需要 3.79ms。

fdopendir+readdir的作用是getdents添加一个缓冲区分配/释放(0.1μs)并检查stat所提供的 fd 是否属于目录类型(0.4μs)。readdir然后对每个目录条目进行一次廉价调用(移动缓冲区中的一个位置并可能重新填充)。

因此,一次性开销为 0.5μs,对于空目录来说是目录扫描时间的 10%,但对于 100 项目录来说只有 1%,对于较大的目录来说几乎可以忽略不计。如果您不需要 fdopen,则此开销会减少 5 倍(仅分配/释放成本)。 (如果不能diropen直接使用,则只需要 fdopen,因此必须通过单独获取的(例如,openat'ted)文件描述符)。

因此,如果您将自定义的一次性分配缓冲区与 一起使用getdents,则可以节省 2-10% 的扫描成本空的目录,而较大的目录则几乎可以忽略不计。

至于readdir调用,现代硬件上PLT间接的成本通常小于1ns,函数调用开销约为1-2ns。鉴于目录扫描时间为微秒量级,您需要至少进行 1000 次readdir调用才能将这些因素转换为单个 µs,但扫描成本为 340µs,并且累积的 1 个额外 µs 约为 0.3%其中——影响可以忽略不计。内联这些readdir(从而消除调用开销和 PLT 开销)只会用于扩展代码,但不会太大提高性能,因为这getdents是那里的瓶颈。

(readdir_r由于额外的锁定,成本更高,但您不需要,readdir_r因为普通readdir调用通常是线程安全的除非你有多个线程调用它们相同的目录流。 POSIX 可能还没有明确说明这一点,但我相信鉴于 glibc 已经弃用了 ,这种保证应该很快就会标准化readdir_r。)

答案2

像和friends这样的函数readdir()是在libc中实现的,libc是一个共享库。与所有共享库一样,这会添加一些重定向,以便能够解析共享库内函数的内存地址。

第一次执行任何特定的库调用时,动态链接器需要在哈希表内查找库调用的地址。这涉及至少一次(但可能更多)字符串比较,这是一种相对昂贵的方法。然后将找到的地址保存在PLT(过程链接表)中,以便下次调用该函数时,查找该函数的开销减少到3条指令(在x86架构上,比其他一些架构上更少)。这就是为什么将某些内容编译为共享对象(而不是静态对象)会产生一些开销。有关共享库开销以及共享库如何在 Linux 上工作的更多信息,请参阅Ulrich Drepper 关于该主题的详细技术解释

syscall()函数本身是在 libc 中实现,因此它也具有重定向功能。但是,由于您只会使用该函数(而不使用其他函数),因此动态链接器要做的工作较少。此外,特定函数的实现例如readdir必须在退出syscall()函数时转换返回值并进行错误检查等,这是一些额外的开销。直接运行的程序syscall()将使用系统调用的直接返回值,并且不需要该转换(它仍然需要进行错误检查,这将使函数显着复杂化)。

直接运行的缺点syscall()是您会转向可移植性较差的 API。联机syscall()帮助页解释了 libc 为您处理的一些特定于体系结构的约束;如果你syscall()直接使用,你的函数可能会在你正在处理的架构上运行,但在ARM机器上会失败。

一般来说,我建议不要syscall()直接使用 API,这与我建议不要直接用汇编语言编写代码的原因相同。是的,这最终可能会更快,但维护负担会变得更高。您可以做一些事情:

  • 不关心性能。系统变得越来越便宜,在许多情况下,“添加另一个系统以使事情进展得更快”比“支付程序员的小时费率以提高性能”更便宜。
  • 对于性能至关重要的一些小事情,针对静态库而不是使用共享库编译软件(即gcc -static
  • 使用分析器查看哪些地方进展缓慢,并重点关注那些事情,而不是担心如何进行系统调用。

答案3

是否有任何具体解释为什么“直接内核调用”会明显更快

我认为这实际上很重要的臭名昭著的情况是,如果您的文件系统没有进行足够的预读,考虑到目录的大小和磁盘设备的每个请求延迟。即,这在繁忙的磁盘(长请求队列)或通过网络访问的磁盘上可能更高。

http://be-n.com/spw/you-can-list-a-million-files-in-a-directory-but-not-with-ls.html

对于大多数用途,glibc 使用的缓冲量不会造成任何问题。指出这种极端情况作为绕过通常缓冲代码的理由,可能会导致“过早优化”或类似的烦恼:-)。

https://github.com/BurntSushi/walkdir/issues/108


如果您不太厌倦阅读 Linus 的一篇文章,他对文件系统目录预读有一些评论。他们可能会或可能不会透露。

https://lore.kernel.org/lkml/[电子邮件受保护]/T/#u

ext4_readdir() 没有被改变来满足 Linus 的咆哮。我也没有看到他想要的方法在其他文件系统的 readdir() 中使用。我认为 XFS 也对目录使用(物理索引的)缓冲区高速缓存[至少这意味着它不能不受益于预读实现,如果目录是碎片的]。 bcachefs 根本不使用 readdir() 的页面缓存;它使用自己的 btree 缓存。我可能在 btrfs 中遗漏了一些东西。

相关内容