bigywater.tech
Go · 93

bufio:缓冲大小对读写性能的影响

bufio 的缓冲区到底有多大作用?设多大合适?忘了 Flush 会怎样?这篇用 50 万次小写和 128 MiB 的逐字节读,把缓冲区大小对系统调用次数和耗时的影响量出来,并复现 Scanner 的行长限制。

976.5 → 6.0 ms
50 万次 16 字节小写:不缓冲 对 默认 4096 字节的 bufio.Writer
3712 字节
没有 Flush 就 Close 时,16000 字节里丢掉的部分
65536
bufio.MaxScanTokenSize:默认 Scanner 一行最长 64 KiB

写:缓冲区越大,系统调用越少

实测 · 本机执行输出
  写 500000 次 16 字节(共 8 MB)到文件:不缓冲                  对底层 Write 的调用  500000 次,耗时   976.5 ms
  写 500000 次 16 字节(共 8 MB)到文件:bufio 16 B           对底层 Write 的调用  500000 次,耗时   964.0 ms
  写 500000 次 16 字节(共 8 MB)到文件:bufio 512 B          对底层 Write 的调用   15625 次,耗时    31.1 ms
  写 500000 次 16 字节(共 8 MB)到文件:bufio 4096 B(默认)     对底层 Write 的调用    1954 次,耗时     6.0 ms
  写 500000 次 16 字节(共 8 MB)到文件:bufio 64 KiB         对底层 Write 的调用     123 次,耗时     2.7 ms
  写 500000 次 16 字节(共 8 MB)到文件:bufio 1 MiB          对底层 Write 的调用       8 次,耗时     2.5 ms

50 万次 16 字节的小写,不缓冲就是 50 万次系统调用,约 2 µs 一次,共 976.5 ms;默认的 4096 字节缓冲区只发出 1954 次调用,6.0 ms;64 KiB 起只剩 123 次、2.7 ms,再大也只有微小的改善。缓冲区和单次写入同样大(16 字节)时等于没有缓冲。

Flush

实测 · 本机执行输出
  写了 16000 字节,没有 Flush 就 Close 文件:磁盘上的文件大小 = 12288 字节(缓冲区里还剩 3712 字节没写出去)

bufio.Writer 不会在 Close 文件时自动 Flush:16000 字节只落盘了 12288 字节,3712 字节留在缓冲区里被丢掉,而且没有任何错误。写完要 Flush,或者用 defer 并检查它的错误。

读:瓶颈会转移

实测 · 本机执行输出
  读 128 MiB 文件(整块 Read):直接用 1 MiB 缓冲区 Read       对底层 Read 的调用      129 次,耗时     8.8 ms
  读 128 MiB 文件(逐字节 ReadByte):bufio 16 B               对底层 Read 的调用  8388609 次,耗时  3865.6 ms
  读 128 MiB 文件(逐字节 ReadByte):bufio 512 B              对底层 Read 的调用   262145 次,耗时   391.6 ms
  读 128 MiB 文件(逐字节 ReadByte):bufio 4096 B(默认)         对底层 Read 的调用    32769 次,耗时   288.0 ms
  读 128 MiB 文件(逐字节 ReadByte):bufio 65536 B            对底层 Read 的调用     2049 次,耗时   274.3 ms
  读 128 MiB 文件(逐字节 ReadByte):bufio 1048576 B          对底层 Read 的调用      129 次,耗时   274.6 ms

逐字节读 128 MiB,16 字节缓冲区要 838 万次系统调用、3.9 秒;4 KiB 起降到约 280 ms 就不再下降,因为剩下的是每个字节一次 ReadByte 调用的成本。对比直接用 1 MiB 缓冲区整块 Read 只要 8.8 ms:能整块处理就别逐字节。(文件已在页缓存里,所以这里没有磁盘等待的成分。)

Scanner 与 Reader

实测 · 本机执行输出
  默认 Scanner 读到一行 100000 字节的内容:读到的行长 = [5],sc.Err() = bufio.Scanner: token too long
  sc.Buffer(…, 1 MiB) 之后:读到的行长 = [5 100000 5],sc.Err() = <nil>
  bufio.MaxScanTokenSize = 65536
  bufio.Reader.ReadString('\n') 没有行长限制:读到的行长(含换行符)= [6 100001 6]

默认 Scanner 的行长上限是 65536 字节,超过就停下并返回 bufio.Scanner: token too long,读不到后面的行;用 sc.Buffer 调大。bufio.Reader 的 ReadString 则没有这个限制,但也要自己防止恶意的超长行耗尽内存。

分步演示

bufio 缓冲
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。写实验:50 万次 16 字节写到临时文件,每种 5 次取中位数;读实验:128 MiB 的临时文件(已在页缓存中),逐字节 ReadByte 对比整块读;底层调用次数由计数包装器统计;所有临时文件实验后已删除
    • 没有验证:文件不在页缓存时的结果、Linux 上的系统调用成本差异、Scanner 其它分割函数、bufio.Writer 对网络连接的影响(Nagle 等)、更大的写入块(远超缓冲区)时的直通行为
    京ICP备2026067502号-1京公网安备11010502063487号