主题
Go 常见优化点:把成本降下来,也把语义保住
优化最常见的失误,是还不知道哪个成本占主导,就开始换容器、加对象池或替换 JSON 库。一次修改也许让微基准好看,却留下更大的常驻内存、更长的排队时间或不同的协议行为。
这篇按证据选择改法:分配多时检查容量与逃逸,字符串中间对象多时用 Builder,等待多时检查锁与批量边界,反射和 JSON 真在热路径上时再评估替代方案,最后用 PGO 把代表性 CPU profile 交回编译器。例子均在 Go 1.27.2、Linux/amd64 运行;没有对线上进程做采样或压测。
先选目标,再看哪个改动值得做
| 看到的证据 | 候选改动 | 主要收益来源 | 要同时检查的代价 |
|---|---|---|---|
| allocs 中 slice 扩容占比高 | 已知规模时预分配 | 少扩容、少复制中间数组 | 过度预留、输入长度攻击、nil 与空切片差异 |
| 相同临时对象反复分配 | sync.Pool | 复用可丢弃的缓冲 | 常驻容量、所有权、脏数据、池中对象可随时消失 |
| 编译器报告不必要逃逸 | 缩小生命周期、改变数据组织 | 减少堆对象和 GC 工作 | 大值复制、接口设计复杂化;不保证改成指针更快 |
| 字符串循环拼接制造大量对象 | strings.Builder、必要时 Grow | 减少中间字符串 | Builder 不能复制,过大 Grow 也占内存 |
| block/mutex profile 指向共享热点 | 缩短临界区、分片或本地聚合 | 少等待、少竞争 | 一致性与可见性改变;分片仍可能有热点 |
| 每条操作都有固定调用/锁/网络成本 | 有界批处理 | 把固定成本摊到多条记录 | 等待成批、部分失败、重试幂等、内存上限 |
| CPU profile 指向动态字段查找 | 缓存元数据、直接访问、代码生成 | 少反射查找和动态处理 | 更复杂的维护与生成流程 |
| JSON 编解码是实测热点 | 调整模型、流式 API、兼容替代 | 少中间表示或更合适实现 | 数字、空值、重复键、大小写、错误等兼容性 |
| 主要成本是可优化的 CPU 热路径 | PGO | 给编译器真实热点信息 | profile 代表性、更新、构建可复现性 |
表格是选项,不是固定优化顺序。若主要时间在数据库锁等待,换 JSON 库可能几乎不改变请求延迟。先保存基线,修改一个主要变量,验证正确性,再用同等负载看目标指标和副作用。[1]
正在绘制图示…
查看图源代码
flowchart LR
A[目标:例如 p99 或 CPU/请求] --> B[代表性工作量和基线]
B --> C[CPU / allocs / heap / block / trace 证据]
C --> D[一个可解释的修改]
D --> E[行为测试与 race]
E --> F[重复性能测量]
F --> G{目标改善且副作用可接受?}
G -->|是| H[受控发布并保留回退]
G -->|否| B
预分配:先知道规模,也知道返回值该是什么
两个函数做同一件事:返回 0 到 n-1。区别只在于是否提前给出容量。
go
func GrowSlice(n int) []int {
var out []int
for i := 0; i < n; i++ { out = append(out, i) }
return out
}
func ReserveSlice(n int) []int {
out := make([]int, 0, n)
for i := 0; i < n; i++ { out = append(out, i) }
return out
}追加超出容量时,需要获得新的底层存储并复制旧内容;具体扩容容量是实现细节,不应把“始终翻倍”写成语言保证。已知最终数量时,预留恰当容量可以消掉中间增长。[2]
本次每个变体各测 10 次、100 ms/次、GOMAXPROCS=1,slice 结果都写入相同全局 sink,明确模拟返回值逃到调用方保存的情形:
| n=1024 的变体 | ns/op 中位数 | B/op | allocs/op |
|---|---|---|---|
| 逐步增长 | 2873,±7% | 25208 | 11 |
| 预留 1024 容量 | 1277,±8% | 8192 | 1 |
这台机器的 int 是 8 字节,最终 1024 个 int 需要 8192 字节。逐步增长的累计分配大于最终数组,是中间数组也算进来了。测试里的 AllocsPerRun 调用点还测到 9 对 1 次分配;调用点与编译优化上下文不同,次数不是 API 承诺。这也说明不能从一处的分配数字机械推到所有调用方。
预分配还有一个很小、却真实的语义变化:
text
zero length nil: grow=true reserve=falsen=0 时,前者返回 nil,后者返回非 nil 的空切片。业务若区分二者,或 JSON 协议对 null 与 [] 有约定,就需要保留原语义。n 来源于不可信输入时,还必须检查上限和溢出,不能为了省分配就允许一次预留任意大容量。
减少逃逸:检查原因,不追求“全都上栈”
局部变量取地址,并不自动意味着分配在堆上;关键在于编译器是否能证明地址不会在栈帧失效后继续被使用。下面两个函数都取了 n 的地址,但生命周期不同。为使演示边界稳定,代码显式禁止这两个小函数内联。
go
//go:noinline
func Local(n int) int { p := &n; return *p }
//go:noinline
func Escaped(n int) *int { return &n }测试把第二个返回的指针保存起来,真实输出是:
text
int allocations: local=0 escaped=1go test -gcflags='-m=2' -run='^$' . 同时报告 Escaped 中的 n escapes to heap、moved to heap: n。这是当前实现的编译决策,代码形状、内联、调用者用途和工具链都会影响它。生产代码不要为了复现这个数字随意加 //go:noinline。[3]
实用动作是检查热路径上是否把短命值装入长期容器,是否为很小的数据建立不必要的指针图,是否有无意的闭包捕获。减少堆对象通常也减少 GC 需要处理的工作,但强行按值复制大对象可能增加 CPU 和栈成本。把 -m 当解释器,和 -benchmem、profile 一起用,别把“出现 escapes”本身当成 bug。[1][3]
sync.Pool:借用期间只有一个所有者
sync.Pool 用于复用临时对象,不是可靠缓存、固定容量资源池或连接池。Get 可以忽略池中已有对象;任何池中对象都可能被移除,所以程序必须在一次复用也没发生时仍然正确。New 常返回指针,以避免把非指针值装进接口时引入额外分配。[4]
本文的 EncodePooled 借一个 *bytes.Buffer,Reset 后交给 JSON Encoder,返回前复制结果,只有容量不超过 64 KiB 的缓冲才归还。
go
b := buffers.Get().(*bytes.Buffer)
b.Reset()
defer func() {
if b.Cap() <= 64<<10 { b.Reset(); buffers.Put(b) }
}()
if err := json.NewEncoder(b).Encode(v); err != nil { return nil, err }
return bytes.Clone(b.Bytes()), nil复制看似又多了一次分配,却解决所有权:调用方拿到的字节不能随着下一次借用被覆盖。测试保存第一次结果,再编码十次不同数据,断言第一次结果没有变化。对象放回 Pool 之后,旧持有者就不能继续访问它。[4][5]
正在绘制图示…
查看图源代码
flowchart LR
P[Pool:可随时丢弃临时对象] -->|Get| O[当前借用者独占 Buffer]
O --> W[Reset 并写入]
W --> C[复制结果交给调用方]
W --> Q{容量是否超过本例 64 KiB 上限?}
Q -->|否| R[Reset 后 Put]
R --> P
Q -->|是| D[不归还,让对象可被回收]
64 KiB 是本例的选择,不是通用最佳值。应按大小分布与复用命中情况决定;把偶发的 20 MiB 缓冲长期留在池里,可能降低分配次数,却提高存活内存。Reset 也不是安全擦除,敏感内容是否允许复用、是否需要清零,要按数据处理要求判断。本篇只验证 Pool 用法的正确性,没有声称这一版编码一定比 json.Marshal 更快。
strings.Builder:减少中间字符串,别复制已使用的 Builder
循环 s += part 不断产生新字符串,Builder 可以集中累积。若能低成本知道总长度,先 Grow(total),还能避免其自身多次扩容。计算总长度也要工作,因此短字符串、两三个片段不一定值得额外一遍扫描。[6]
本次拼接 128 个 "go中",每种 10 次的摘要:
| Builder 写法 | ns/op 中位数 | B/op | allocs/op |
|---|---|---|---|
| 不提前 Grow | 797.4,±2% | 1912 | 8 |
| 按总字节数 Grow | 571.4,±12% | 640 | 1 |
这里收益来自特定输入规模。一个已经使用过的 Builder 不能按值复制;负向测试实际触发并 recover 了:
text
copy: strings: illegal use of non-zero Builder copied by value需要在函数间传递它时用指针,并保持清楚的生命周期;不要把带 Builder 的结构体悄悄复制。返回字符串后,长期保留一个很短的字符串是否连带留住过大的底层存储,也值得用 heap profile 检查。[6]
锁竞争与批处理:少拿几次锁,改变的可能还有可见性
如果 mutex profile 表明一把共享锁让大量 goroutine 等待,先缩短临界区:移走 I/O、日志格式化和可在锁外完成的计算。不是看见锁就改成 RWMutex;读写比、持锁时长、缓存一致性和热点分布都影响结果。[4]
本例累计一组整数,先在本地求和,最后只拿一次锁:
go
func (c *Counter) AddBatch(xs []int) {
sum := 0
for _, x := range xs { sum += x }
c.mu.Lock()
c.n += sum
c.mu.Unlock()
}与每个元素都 Lock/Unlock 的版本比较,4 个 goroutine 分别添加 [1,2,3],两者最终值都被断言为 24,race 检查通过。这里验证的是最终总和;逐条更新原本可能被其他读者观察到,批量更新只在批次完成时可见,不能把它当作任何业务语义下都完全等价。
真实批处理还需要“最多多少条、最多等多久”两个上限:只设条数,低流量请求可能一直等;只设时间,高流量会堆很大一批。批量数据库/网络调用要定义部分失败和幂等重试,停机时要 flush 或明确舍弃策略。分片计数器、单 goroutine 拥有状态、atomic、RWMutex 都各有适用条件;用 profile 与代表性并发测试决定,别让一个热点锁变成一个热点队列。
反射与 JSON:先看热点,再看兼容性
反射常见成本来自动态查找、类型判断、接口装箱和无法利用的静态信息。例如按名字读字段:
go
reflect.ValueOf(v).FieldByName("ID").Int()如果类型已经固定,直接 v.ID 更简单;类型动态但模式有限,可以缓存按 reflect.Type 建立的字段元数据,避免每条记录重新查找。需要大量已知 schema 时再考虑代码生成,同时付出生成器版本、产物更新和调试成本。本文测试了直接读取与反射读取结果一致,没有给未测的反射场景编造加速倍数。[7]
JSON 更不能仅靠把 import 换掉结束评估。Go 1.27.2 的 encoding/json 与 encoding/json/v2 是当前就能复现语义差异的一对例子:
text
v1: {"id":0,"tags":null}
v2: {"id":0,"tags":[]}
uppercase key: v1=7 v2=0
duplicate rejected: v1=false v2=true结构体标签是 json:"id"。输入 {"ID":7} 在 v1 被大小写不敏感匹配,在 v2 的默认行为下没有匹配;重复 id 键在 v1 默认接受、后值覆盖,在 v2 默认拒绝。[8]
| 替换或升级前的回归集 | 为什么重要 |
|---|---|
| nil/空切片与 map,omitempty/omitzero | 影响 null、[]、{}、字段是否出现 |
| 大整数、浮点边界、解码到 any | 避免 ID 精度与类型变化 |
| 大小写、重复键、未知字段 | 影响兼容性、安全检查与容错 |
| 无效 UTF-8、HTML 转义、map 顺序 | 影响外部协议、签名/缓存键和文本比较 |
| 自定义 Marshaler/Unmarshaler | 外部库是否遵守项目现有约定 |
| 流式输入、错误时部分修改、最大输入长度 | 影响内存上限、恢复与攻击面 |
先锁定这些行为,再基准相同语义和输入。解码到具体结构体、避免先 map[string]any 再转一遍,可能比换库更容易保持语义;流式 API 有助于控制中间缓冲,但不是自动的总输入上限。任何第三方替代还要检查维护、漏洞、平台支持和依赖成本。本篇没有用语义不同的两个 JSON 默认实现做速度排名。
PGO:把代表性 profile 变成构建输入
PGO 使用 CPU pprof profile 帮助编译器判断热点,例如决定哪些调用值得更积极内联。它不使用 heap profile 来“学习内存”,也不会修复锁竞争、数据库慢查询或算法错误。[9]
常用入口有三个:主包目录的 default.pgo 由默认 -pgo=auto 发现;-pgo=/absolute/path/cpu.pprof 显式选文件;-pgo=off 明确关闭,便于基线比较。显式路径会作用到该构建命令涉及的主包,多个不同程序通常应分别选 profile。[9]
本次真实执行了以下流程:短 CPU 基准生成 train.pprof → 当前 pprof 成功解析 → go test -pgo=... 通过全部测试 → 生成 PGO 测试二进制。训练样本只覆盖本文的字符串小例子,所以这里只验证“采样到构建”的链路,没有声称得到生产收益。
生产流程应选有代表性的请求比例、输入大小和运行阶段,保存采样版本、时长与权重。把不同时长的 profile 简单相加会让长窗口贡献更多样本,合并前要确认这正是想要的权重。代码大幅重构、业务流量变化后要更新 profile;源码、工具链和 profile 一起固定,构建才有可复现的依据。再用独立于训练输入的工作量比较 PGO 开关,避免只在训练小例子上验证。[9]
本次测量怎样使用
上述时间摘要来自 AMD EPYC 9V74、共享 Linux 云机器,标准优化构建、每变体 10 次、100 ms、-cpu=1;benchstat 固定到 v0.0.0-20261009192801-be2c69fb417e。这里展示各变体的中位数和 95% 置信区间,没有做成对 A/B 显著性断言。完整原始行见文末。race 仅用于独立正确性检查,不与普通构建计时混在一起。
测试能证明这些输入的结果、所有权和反例行为,不能证明对象池命中率、生产 p99 或新 JSON 实现的全面兼容性。下一次优化最值得留下的记录,是“哪份证据让我们选了这条改法”,而不只是最终少了几个纳秒。
依据
- Go diagnostics、Go GC guide:按成本选诊断方式与分配/GC 权衡。
- Go 规范:append;Go 1.27.2 slice 实现:容量增长与实现边界。
- Go FAQ:堆或栈;本机 Go 1.27.2
-gcflags=-m=2输出。 - Go 1.27.2 sync:Pool、Mutex、RWMutex 约定。
- Go 1.27.2 bytes.Buffer:Bytes 的共享存储与有效期。
- Go 1.27.2 strings.Builder,固定源码:Grow、String、copyCheck。
- Go 1.27.2 reflect:动态字段访问 API,速度不作未测推断。
- Go 1.27.2 encoding/json 与 encoding/json/v2:迁移差异与默认语义。
- Go 官方 PGO 文档:CPU profile、代表性、构建入口、合并权重与版本偏差。
完整源码、测试与脚本
使用 Go 1.27.2,无第三方运行依赖。按文件名保存后执行 bash run.sh out。测试故意触发的 Builder panic 已局部 recover,只验证禁止复制的行为。PGO profile 是脚本生成的输入文件,不需要从别处下载。
go.mod
go.mod
module example.com/optimizelab
go 1.27.2optimize.go
代码块 · 93 行 · 点击收起 / 展开
go
package optimizelab
import (
"bytes"
"encoding/json"
"reflect"
"strings"
"sync"
)
func GrowSlice(n int) []int {
var out []int
for i := 0; i < n; i++ {
out = append(out, i)
}
return out
}
func ReserveSlice(n int) []int {
out := make([]int, 0, n)
for i := 0; i < n; i++ {
out = append(out, i)
}
return out
}
func Build(parts []string, reserve bool) string {
var b strings.Builder
if reserve {
n := 0
for _, p := range parts {
n += len(p)
}
b.Grow(n)
}
for _, p := range parts {
b.WriteString(p)
}
return b.String()
}
var buffers = sync.Pool{New: func() any { return new(bytes.Buffer) }}
func EncodePooled(v any) ([]byte, error) {
b := buffers.Get().(*bytes.Buffer)
b.Reset()
defer func() {
if b.Cap() <= 64<<10 {
b.Reset()
buffers.Put(b)
}
}()
if err := json.NewEncoder(b).Encode(v); err != nil {
return nil, err
}
// The caller owns this copy; the buffer is reusable after return.
return bytes.Clone(b.Bytes()), nil
}
type Counter struct {
mu sync.Mutex
n int
}
func (c *Counter) AddOneByOne(xs []int) {
for _, x := range xs {
c.mu.Lock()
c.n += x
c.mu.Unlock()
}
}
func (c *Counter) AddBatch(xs []int) {
sum := 0
for _, x := range xs {
sum += x
}
c.mu.Lock()
c.n += sum
c.mu.Unlock()
}
func (c *Counter) Value() int { c.mu.Lock(); defer c.mu.Unlock(); return c.n }
type Record struct {
ID int `json:"id"`
Tags []string `json:"tags"`
}
func DirectID(v Record) int { return v.ID }
func ReflectID(v Record) int { return int(reflect.ValueOf(v).FieldByName("ID").Int()) }
//go:noinline
func Local(n int) int { p := &n; return *p }
//go:noinline
func Escaped(n int) *int { return &n }optimize_test.go
代码块 · 151 行 · 点击收起 / 展开
go
package optimizelab
import (
"bytes"
"encoding/json"
jsonv2 "encoding/json/v2"
"fmt"
"reflect"
"strings"
"sync"
"testing"
)
var sliceSink []int
var intSink *int
func TestBehavior(t *testing.T) {
for _, n := range []int{1, 16, 1024} {
if !reflect.DeepEqual(GrowSlice(n), ReserveSlice(n)) {
t.Fatal(n)
}
}
parts := []string{"a", "中", "b"}
if Build(parts, false) != Build(parts, true) {
t.Fatal("builder")
}
for _, add := range []func(*Counter, []int){(*Counter).AddOneByOne, (*Counter).AddBatch} {
c := new(Counter)
var wg sync.WaitGroup
for range 4 {
wg.Go(func() { add(c, []int{1, 2, 3}) })
}
wg.Wait()
if c.Value() != 24 {
t.Fatal(c.Value())
}
}
first, err := EncodePooled(Record{ID: 1})
if err != nil {
t.Fatal(err)
}
saved := bytes.Clone(first)
for range 10 {
if _, err := EncodePooled(Record{ID: 9}); err != nil {
t.Fatal(err)
}
}
if !bytes.Equal(first, saved) {
t.Fatal("returned bytes were reused")
}
if DirectID(Record{ID: 7}) != ReflectID(Record{ID: 7}) {
t.Fatal("reflect")
}
}
func TestAllocationEvidence(t *testing.T) {
a := testing.AllocsPerRun(20, func() { sliceSink = GrowSlice(1024) })
b := testing.AllocsPerRun(20, func() { sliceSink = ReserveSlice(1024) })
if b >= a {
t.Fatalf("expected fewer allocations: %v %v", a, b)
}
fmt.Printf("slice allocations: grow=%.0f reserve=%.0f\n", a, b)
a = testing.AllocsPerRun(20, func() { _ = Local(7) })
b = testing.AllocsPerRun(20, func() { intSink = Escaped(7) })
fmt.Printf("int allocations: local=%.0f escaped=%.0f\n", a, b)
if a != 0 || b != 1 {
t.Fatal(a, b)
}
}
func TestBuilderCopyPanics(t *testing.T) {
var b strings.Builder
b.WriteString("a")
c := b
defer func() {
if r := recover(); r == nil {
t.Fatal("expected copied-builder panic")
} else {
fmt.Println("copy:", r)
}
}()
c.WriteString("b")
}
func TestJSONDifferences(t *testing.T) {
a, err := json.Marshal(Record{})
if err != nil {
t.Fatal(err)
}
b, err := jsonv2.Marshal(Record{})
if err != nil {
t.Fatal(err)
}
fmt.Println("v1:", string(a))
fmt.Println("v2:", string(b))
var x, y Record
if err := json.Unmarshal([]byte(`{"ID":7}`), &x); err != nil {
t.Fatal(err)
}
if err := jsonv2.Unmarshal([]byte(`{"ID":7}`), &y); err != nil {
t.Fatal(err)
}
fmt.Printf("uppercase key: v1=%d v2=%d\n", x.ID, y.ID)
if x.ID != 7 || y.ID != 0 {
t.Fatal(x, y)
}
err1 := json.Unmarshal([]byte(`{"id":1,"id":2}`), &x)
err2 := jsonv2.Unmarshal([]byte(`{"id":1,"id":2}`), &y)
fmt.Printf("duplicate rejected: v1=%v v2=%v\n", err1 != nil, err2 != nil)
if err1 != nil || err2 == nil {
t.Fatal(err1, err2)
}
}
func BenchmarkSlice(b *testing.B) {
for _, name := range []string{"grow", "reserve"} {
b.Run(name, func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
if name == "grow" {
sliceSink = GrowSlice(1024)
} else {
sliceSink = ReserveSlice(1024)
}
}
})
}
}
func BenchmarkBuilder(b *testing.B) {
parts := make([]string, 128)
for i := range parts {
parts[i] = "go中"
}
for _, reserve := range []bool{false, true} {
b.Run(fmt.Sprint(reserve), func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
_ = Build(parts, reserve)
}
})
}
}
func BenchmarkHot(b *testing.B) {
parts := []string{"alpha", "中", "omega"}
for b.Loop() {
_ = Build(parts, true)
}
}
func TestZeroLengthSemantics(t *testing.T) {
fmt.Printf("zero length nil: grow=%v reserve=%v\n", GrowSlice(0) == nil, ReserveSlice(0) == nil)
if GrowSlice(0) != nil || ReserveSlice(0) == nil {
t.Fatal("unexpected zero length behavior")
}
}run.sh
bash
#!/usr/bin/env bash
set -euo pipefail
export TZ=UTC
out=${1:-out}
mkdir -p "$out"
{ go version; go env GOOS GOARCH CGO_ENABLED; grep -m1 'model name' /proc/cpuinfo; } > "$out/environment.txt"
go test -v . > "$out/test.txt"
go test -race . > "$out/race.txt"
go vet . > "$out/vet.txt"
go test -run='^$' -bench='Benchmark(Slice|Builder)$' -benchmem -benchtime=100ms -count=10 -cpu=1 > "$out/bench.txt"
go test -run='^$' -bench='^BenchmarkHot$' -pgo=off -benchtime=500ms -cpu=1 -cpuprofile="$out/train.pprof" -o "$out/train.test" > "$out/train.txt"
go tool pprof -top "$out/train.test" "$out/train.pprof" > "$out/train-top.txt"
go test -pgo="$PWD/$out/train.pprof" . > "$out/pgo-test.txt"
go test -c -pgo="$PWD/$out/train.pprof" -o "$out/pgo.test" .
go test -gcflags='-m=2' -run='^$' . > "$out/escape-stdout.txt" 2> "$out/escape.txt"统计摘要命令
bash
mkdir -p bin
GOBIN="$PWD/bin" GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org \
go install golang.org/x/perf/cmd/benchstat@v0.0.0-20261009192801-be2c69fb417e
./bin/benchstat out/bench.txt本次全部性能测量行
代码块 · 40 行 · 点击收起 / 展开
text
BenchmarkSlice/grow 36056 3062 ns/op 25208 B/op 11 allocs/op
BenchmarkSlice/grow 37762 3043 ns/op 25208 B/op 11 allocs/op
BenchmarkSlice/grow 41712 3073 ns/op 25208 B/op 11 allocs/op
BenchmarkSlice/grow 39757 2887 ns/op 25208 B/op 11 allocs/op
BenchmarkSlice/grow 44893 2808 ns/op 25208 B/op 11 allocs/op
BenchmarkSlice/grow 39680 2858 ns/op 25208 B/op 11 allocs/op
BenchmarkSlice/grow 42763 2809 ns/op 25208 B/op 11 allocs/op
BenchmarkSlice/grow 40262 2804 ns/op 25208 B/op 11 allocs/op
BenchmarkSlice/grow 41823 2816 ns/op 25208 B/op 11 allocs/op
BenchmarkSlice/grow 41467 2958 ns/op 25208 B/op 11 allocs/op
BenchmarkSlice/reserve 83647 1444 ns/op 8192 B/op 1 allocs/op
BenchmarkSlice/reserve 88416 1381 ns/op 8192 B/op 1 allocs/op
BenchmarkSlice/reserve 94906 1253 ns/op 8192 B/op 1 allocs/op
BenchmarkSlice/reserve 95439 1277 ns/op 8192 B/op 1 allocs/op
BenchmarkSlice/reserve 96876 1304 ns/op 8192 B/op 1 allocs/op
BenchmarkSlice/reserve 97070 1257 ns/op 8192 B/op 1 allocs/op
BenchmarkSlice/reserve 92954 1285 ns/op 8192 B/op 1 allocs/op
BenchmarkSlice/reserve 98426 1277 ns/op 8192 B/op 1 allocs/op
BenchmarkSlice/reserve 96687 1240 ns/op 8192 B/op 1 allocs/op
BenchmarkSlice/reserve 96697 1274 ns/op 8192 B/op 1 allocs/op
BenchmarkBuilder/false 148002 806.6 ns/op 1912 B/op 8 allocs/op
BenchmarkBuilder/false 154075 794.8 ns/op 1912 B/op 8 allocs/op
BenchmarkBuilder/false 150734 800.6 ns/op 1912 B/op 8 allocs/op
BenchmarkBuilder/false 145501 775.4 ns/op 1912 B/op 8 allocs/op
BenchmarkBuilder/false 154795 789.2 ns/op 1912 B/op 8 allocs/op
BenchmarkBuilder/false 150596 797.2 ns/op 1912 B/op 8 allocs/op
BenchmarkBuilder/false 159316 797.6 ns/op 1912 B/op 8 allocs/op
BenchmarkBuilder/false 166291 785.2 ns/op 1912 B/op 8 allocs/op
BenchmarkBuilder/false 148338 802.0 ns/op 1912 B/op 8 allocs/op
BenchmarkBuilder/false 157102 821.4 ns/op 1912 B/op 8 allocs/op
BenchmarkBuilder/true 241076 530.6 ns/op 640 B/op 1 allocs/op
BenchmarkBuilder/true 207992 612.3 ns/op 640 B/op 1 allocs/op
BenchmarkBuilder/true 234565 565.5 ns/op 640 B/op 1 allocs/op
BenchmarkBuilder/true 222883 560.8 ns/op 640 B/op 1 allocs/op
BenchmarkBuilder/true 199386 646.8 ns/op 640 B/op 1 allocs/op
BenchmarkBuilder/true 176230 623.7 ns/op 640 B/op 1 allocs/op
BenchmarkBuilder/true 222014 566.2 ns/op 640 B/op 1 allocs/op
BenchmarkBuilder/true 224803 576.6 ns/op 640 B/op 1 allocs/op
BenchmarkBuilder/true 208192 565.3 ns/op 640 B/op 1 allocs/op
BenchmarkBuilder/true 230055 640.7 ns/op 640 B/op 1 allocs/op