bufio 的缓冲区到底有多大作用?设多大合适?忘了 Flush 会怎样?这篇用 50 万次小写和 128 MiB 的逐字节读,把缓冲区大小对系统调用次数和耗时的影响量出来,并复现 Scanner 的行长限制。
写 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 字节)时等于没有缓冲。
写了 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 读到一行 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 则没有这个限制,但也要自己防止恶意的超长行耗尽内存。