返回资讯中心
外部精选
软件工程
#社区热议

开发者提出优化终端行缓冲输出性能的新方案

来源标题:We can do better than line-buffered output

一篇技术文章指出,在非交互终端应用中,行缓冲常成为性能瓶颈,建议将输出重定向到文件或管道来规避。作者以 Info-ZIP unzip 为例,说明打印文件名会拖慢大量小文件的解压速度,并称已在自研的 tugz unzip 中解决该问题,同时不改变用户界面。

Hacker Newszdw2026/11/5 · 今天13 分钟阅读

以下正文由 AI 翻译自 Hacker News(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。

缓冲输出通常有两种形式:全缓冲和行缓冲。当输出指向终端时,实现会自动选择行缓冲,在每行结束时刷新缓冲区,使内容立即送达用户。否则就采用全缓冲,将输出保留到缓冲区写满或将来某个任意时刻发生显式刷新事件为止。行缓冲是非交互式终端应用中常见的瓶颈,以至于值得把输出重定向到文件或管道来规避它。其实不必如此。我们可以两者兼得:以全缓冲的速度将行及时送达终端。

例如,Info-ZIP 的 unzip 在从归档中解压文件时会打印文件名。对于包含大量小文件的归档,这种打印会主导解压时间。有时我不带任何选项运行 unzip,然后终端被文件名淹没,我就知道解压遇到了瓶颈。于是我取消,再用 -q(安静模式)重新开始以避开瓶颈,因为重新开始比等待更快。由于有过这种体验,我在 tugz 的 unzip 中在不改变用户界面的前提下规避了这个问题。你依然能看到文件列表,但不必为此付出代价。 $ unzip example.zip Archive: example.zip inflating: example.bmp inflating: example.svg inflating: example.txt 考虑这个打印三行的 C 程序: #include <stdio.h> int main() puts("a\nb"); puts("c"); 如果我们用 strace 之类的工具检查写入操作: $ strace -o log -e write ./a.out a b c 我们可能会看到(这里是 glibc): write(1, "a\n", 2) = 2 write(1, "b\n", 2) = 2 write(1, "c\n", 2) = 2 是的,glibc 对行缓冲的理解相当字面化,甚至会在一个字符串内部拆分写入。有些实现稍微聪明一些: write(1, "a\nb\n", 2) = 4 write(1, "c\n", 2) = 2 CPython 对这个程序正是这样写入的: import sys sys.stdout.write("a\nb\n") sys.stdout.write("c\n") 它的规则是:在追加新输出后,如果缓冲区中包含换行符就刷新,后果是可能输出不完整的行。 基于前面的例子: enum { BUF_ERR = 1 << 0, BUF_LINE = 1 << 1, }; typedef struct { uint8_t *buf; ptrdiff_t cap; ptrdiff_t len; int fd; int flags; } Buf; bool os_write(int fd, void *, ptrdiff_t); void flush(Buf *b) if (!(b->flags & BUF_ERR) && b->len) { if (!os_write(b->fd, b->buf, b->len)) { b->flags |= BUF_ERR; b->len = 0; 这样的行缓冲可以通过“先检测再刷新”来实现: bool has_newline(Buf *b) int r = 0; for (ptrdiff_t i = 0; i < b->len; i++) { r |= b->buf[i] == '\n'; return r; void append(Buf *b, uint8_t *src, ptrdiff_t len) uint8_t *end = src + len; while (!(b->flags & BUF_ERR) && src<end) { ptrdiff_t left = end - src; ptrdiff_t avail = b->cap - b->len; ptrdiff_t amount = avail<left ? avail : left; memcpy(b->buf+b->len, src, amount); b->len += amount; src += amount; if (amount < left) { flush(b); // make room // test-then-flush if line buffered if ((b->flags & BUF_LINE) && has_newline(b)) { flush(b); 如果我们有一堆行要打印,并把它们一个接一个地追加到缓冲区,那就会产生大量本可以合并为一次的写入。

让我们想想目标:如果输出是终端,我们不希望无限期地扣住行。但这并不等同于每个换行都写入一次。对于“急于发送数据,而底层不感知缓冲的调用方却做了大量小写入”这一问题,其实已有现成方案:Nagle 算法。

也就是说,如果缓冲区已经空闲了一小段时间就刷新它。我们其实并不特别关心换行。问题在于这需要调整接口。给缓冲区加一个截止时间: typedef struct { int64_t deadline; // ... } Buf; int64_t os_monotonic_clock(); void deadline_flush(Buf b) if ((b->flags & BUF_LINE) && b->len) { int64_t now = os_monotonic_clock(); if (b->deadline && now>b->deadline) { flush(b); b->deadline = 0; } else { b->deadline = now + DEADLINE; 其中 DEADLINE 是适合人类感知的值,比如 100ms 左右。我们可以在一个线程中并发地触发它,当有数据写入缓冲区时通过 futex 唤醒。但更好的做法是在短暂的工作间隙触发它,tugz 就是这么做的。例如(使用我的 strings): for (/ filename in filenames /) { Str pre = S(" inflating: "); Str suf = S("\n"); append(&stdout, pre.data, pre.len); append(&stdout, filename.data, filename.len); append(&stdout, suf.data, suf.len); while (/ not done inflating */) { // 1. inflate block // 2. write block deadline_flush(&stdout); 如果它在截止时间内处理了多个文件,它们的名字就会一起输出。无需线程。与传统“遇换行即刷新”相比,行为差异对人类而言无法察觉,除了程序运行更快这一事实。这也是不必再碰糟糕的 C stdio 的又一个理由。

对本文有评论?请通过发送电子邮件至 ~skeeto/public-inbox@lists.sr.ht 在我的公开收件箱中发起讨论 [邮件列表礼仪],或查看现有讨论。

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →