返回首页

Go 竞赛零分配 IO

contestio 库通过零分配技术优化 Go 竞赛的输入输出。使用对 bufio.Reader 的直接访问、泛型解析器和临时缓冲区。基准测试证明其优于 fmt 和 strconv。

contestio:Go 竞赛 IO 的 6 个工程技巧
Advertisement 728x90

Go 比赛零分配 I/O:六大工程解决方案

Go 开发者在竞技编程中经常遇到标准 I/O 工具的瓶颈。fmt.Fscan 每次调用都会分配内存,bufio.Scanner 被锁定在扫描模式,手动用 bufio.Reader 解析则需要写一大堆样板代码。contestio 库通过直接操作 bufio.Reader 的内部缓冲区,实现了所有操作的零分配。

六大核心方案融合了手动解析的速度、fmt 的便利性和 bufio 的灵活性。它能处理混合输入类型——数字、字符串,甚至逐字符解析——无需切换模式。

直接缓冲区访问:无拷贝的 _nextToken

核心技巧是 _nextToken 函数,它从 bufio.Reader 的内部缓冲区返回字节切片。零分配、无拷贝。你仍然可以使用 bufio.Reader 的方法,如 ReadStringPeekReadByte

Google AdInline article slot
var age int
var name string

ScanInt(br, &age)               // 通过 contestio
name, _ = br.ReadString('\n')   // 标准方法
name = strings.TrimSpace(name)

高级的 ScanIntScanWord 与底层调用完美兼容。没有僵硬的“扫描模式”——数据按任意顺序读取即可。

通用整数解析器:利用泛型

一个泛型 _parseInt[T Int] 函数处理 int8uint16int64uint64。编译器为每种类型生成专用代码:

  • 无符号类型跳过符号处理。
  • 嵌入范围常量(int8absMin = 1<<7)。
  • 自定义数字循环支持最多 20 位数字,备选使用 ParseUint
func _parseIntT Int (T, error) {
    unsigned := ^T(0) >= 0
    // 符号处理、数字循环、范围检查
}

汇编显示紧凑优化的代码,无多余检查。多功能无性能损失。

Google AdInline article slot

零分配输出:动态数据的临时缓冲区

写入时使用 bufio.Writer 搭配附带的 scratch [32]byte 数组。如果主缓冲区空间不足,就填充临时缓冲区并一次性刷新。

func _writeAppendFunc(bw *Writer, appendVal func([]byte, T) []byte, v T) error {
    var buf []byte
    if bw.Available() < len(bw.scratch) {
        buf = bw.scratch[:0]
    } else {
        buf = bw.AvailableBuffer()
    }
    buf = appendVal(buf, v)
    _, err := bw.Write(buf)
    return err
}

结果:所有支持类型的零分配输出。

轻量反射:支持任意类型

类型化函数如 ScanInt[T]ScanFloat[T]ScanWord[T] 构成主要 API。为灵活性,ScanAny(br, &product, &price) 使用基于 reflect.Kind 的解析表:

Google AdInline article slot
var _parseAnyTab = []_parseAnyFunc{
    reflect.Int:     _parseIntToAny[int],
    reflect.Int8:    _parseIntToAny[int8],
    reflect.Float32: _parseFloatToAny[float32],
    reflect.String:  _parseWordToAny[string],
}

反射仅用于分发:检查参数类型并调用对应解析器。在循环外传递可复用变量指针——零分配。

var x int
for i := 0; i < N; i++ {
    x = data[i]
    PrintAny(bw, op, &x)
}

Must 标签:通过构建标签实现 Panic 或错误处理

比赛输入通常干净,但检查必不可少。省略每个调用后的 if err != nil { panic(err) }——使用私有 _must 函数。

func _scanSliceT any (int, error) {
    return _must(_scanSliceCommon(br, parse, a))
}

must.go//go:build must)中,_must 遇到错误(除 io.EOF)即 panic。在 nomust.go 中返回 error。编译器内联正确版本。

go run -tags=must main.go   # panic
go run main.go              # 返回错误

内联工具:自包含解决方案

Codeforces 等平台禁止外部包。contestio-inline 分析 AST,仅将用到的 contestio 函数嵌入 main.go

contestio-inline main.go          # 嵌入
contestio-inline -clear main.go   # 还原

需要精确导入 import . "github.com/aaa2ppp/contestio"。工具检查编译、备份文件,并支持 -tags=must

内联优势:

  • 无依赖。
  • 仅所需代码(依赖图)。
  • 你的解决方案代码保持原样。

基准测试:100 万数字,4KB 缓冲区

| 方法 | 读取时间,ms | 分配次数 | 写入时间,ms | 分配次数 |

|--------|---------------|-------------|----------------|-------------|

| fmt.Fscan/Fprint | 473 | 1 005 000 | 109 | 965 000 |

| Scanner+Atoi | 106 | 0 | - | - |

| strconv.AppendInt | - | - | 37 | 2 600 |

| contestio | 62 | 0 | 39 | 0 |

测试环境:Intel Core i3-7100,Go 1.24.2,Windows。

核心要点

  • 零分配 读写,通过直接缓冲区访问。
  • 泛型为每种整数类型生成最优代码。
  • 临时缓冲区实现无分配输出。
  • 反射仅用于分发,非序列化。
  • must 构建标签减少错误检查样板代码。

— Editorial Team

Advertisement 728x90

继续阅读