刘沙河 刘沙河
首页
  • Go语言基础

    • 数据类型
    • 反射
    • Go指针
  • Go语言进阶

    • go泛型
    • go条件编译
    • cgo教程
    • Go协程调度原理及GPM模型
    • Go内存管理
    • Go垃圾回收机制
    • Go语言内存对齐
  • Go语言实现原理

    • channel 实现原理
    • slice 实现原理
    • map 实现原理
    • sync.Mutex 实现原理
    • 乐观锁CAS 实现原理
    • singlefight 实现原理
  • gin框架

    • gin中间件原理
    • gin路由原理
  • gorm

    • GORM介绍和使用
    • GORM_CURD操作指南
  • go测试

    • benchmark基准测试
    • pprof 性能分析
  • python进阶

    • Numpy&Pandas
    • celery分布式任务队列
  • Django

    • Django 常见命令
    • middleware中间件
    • Django缓存系统
    • Django信号系统
    • Django REST Framework
  • Flask

    • Flask基础知识总结
    • Flask-SQLAlchemy
  • 爬虫

    • aiohttp
    • scrapy框架
  • Mysql

    • Mysql存储引擎和索引
    • MySQL主从复制
    • Mysql读写分离
    • 数据库分库分表
    • Mysql锁
    • Mysql事务和MVCC原理
    • 分库分表带来的读扩散问题
  • Redis

    • redis基础和数据类型
    • redis主从架构
    • redis哨兵架构
    • redis集群模式
    • 如何保证缓存和数据库双写一致
    • redis底层数据结构
    • redis分布式锁
  • Elasticsearch

    • es基本概念
    • es基础语法
    • es倒排索引
  • etcd

    • Go操作etcd
    • Raft原理
    • etcd分布式锁
  • kafka

    • 消息队列MQ总结
    • kafka 概述及原理
    • kafka 消费问题记录
    • 零拷贝技术
    • kafka分区规范
  • RabbitMQ

    • rabbitMQ基础
    • Go操作rabbitmq
  • RocketMQ

    • 可靠消息队列 rocketMQ
  • Http&Https

    • http&https
    • TCP和UDP
    • Ping 原理
  • RPC

    • RPC初识
    • grpc初识和实现
  • gRPC

    • grpc 初识
    • grpc 上下文 metadata
    • grpc 健康检查
    • grpc keepalive
    • grpc 命名解析
    • grpc 中间件&拦截器
    • grpc 负载均衡
    • grpc 身份认证
    • grpc 超时重试
    • grpc 链路追踪
    • grpc-gw将gRPC转RESTfu api
    • grpc-gw自定义选项
  • protobuf

    • protobuf 进阶
    • protobuf 编码原理
  • Docker

    • Docker基础
    • Docker常用命令
    • Dockerfile
    • Docker-Compose
    • Docker多阶段构建
    • Docker Config 教程
    • Docker Swarm 教程
    • Docker Stack 教程
    • Docker Buildx 教程
  • k8s

    • k8s 基础概念
    • k8s 集群架构
    • k8s 工作负载
    • Pod 网络
    • Service 网络
    • 外部接入网络
    • 一张图搞懂k8s各种pod
    • k8s 存储抽象
    • mac快速启动k8s
    • 自制申威架构k8s-reloader
  • go-kit

    • go-kit初识
    • go-kit启动http服务
    • go-kit集成gin启动服务
    • go-kit集成grpc和protobuf
    • go-kit中间件
    • go-kit服务注册发现与负载均衡
    • go-kit限流和熔断
    • go-kit链路追踪
    • go-kit集成Prometheus
  • 设计模式

    • 初识设计模式
    • 创建型模式
    • 结构型模式
    • 行为模式
  • 数据结构

    • 时间轮
    • 堆、双向链表、环形队列
    • 队列:优先队列
    • 队列:延迟队列
  • 算法

    • 递归算法
    • 枚举算法
    • 动态规划
    • 回溯算法
    • 分治算法
    • 贪心算法
    • LRU和LFU
    • 一致性哈希

花开半夏,半夏花开
首页
  • Go语言基础

    • 数据类型
    • 反射
    • Go指针
  • Go语言进阶

    • go泛型
    • go条件编译
    • cgo教程
    • Go协程调度原理及GPM模型
    • Go内存管理
    • Go垃圾回收机制
    • Go语言内存对齐
  • Go语言实现原理

    • channel 实现原理
    • slice 实现原理
    • map 实现原理
    • sync.Mutex 实现原理
    • 乐观锁CAS 实现原理
    • singlefight 实现原理
  • gin框架

    • gin中间件原理
    • gin路由原理
  • gorm

    • GORM介绍和使用
    • GORM_CURD操作指南
  • go测试

    • benchmark基准测试
    • pprof 性能分析
  • python进阶

    • Numpy&Pandas
    • celery分布式任务队列
  • Django

    • Django 常见命令
    • middleware中间件
    • Django缓存系统
    • Django信号系统
    • Django REST Framework
  • Flask

    • Flask基础知识总结
    • Flask-SQLAlchemy
  • 爬虫

    • aiohttp
    • scrapy框架
  • Mysql

    • Mysql存储引擎和索引
    • MySQL主从复制
    • Mysql读写分离
    • 数据库分库分表
    • Mysql锁
    • Mysql事务和MVCC原理
    • 分库分表带来的读扩散问题
  • Redis

    • redis基础和数据类型
    • redis主从架构
    • redis哨兵架构
    • redis集群模式
    • 如何保证缓存和数据库双写一致
    • redis底层数据结构
    • redis分布式锁
  • Elasticsearch

    • es基本概念
    • es基础语法
    • es倒排索引
  • etcd

    • Go操作etcd
    • Raft原理
    • etcd分布式锁
  • kafka

    • 消息队列MQ总结
    • kafka 概述及原理
    • kafka 消费问题记录
    • 零拷贝技术
    • kafka分区规范
  • RabbitMQ

    • rabbitMQ基础
    • Go操作rabbitmq
  • RocketMQ

    • 可靠消息队列 rocketMQ
  • Http&Https

    • http&https
    • TCP和UDP
    • Ping 原理
  • RPC

    • RPC初识
    • grpc初识和实现
  • gRPC

    • grpc 初识
    • grpc 上下文 metadata
    • grpc 健康检查
    • grpc keepalive
    • grpc 命名解析
    • grpc 中间件&拦截器
    • grpc 负载均衡
    • grpc 身份认证
    • grpc 超时重试
    • grpc 链路追踪
    • grpc-gw将gRPC转RESTfu api
    • grpc-gw自定义选项
  • protobuf

    • protobuf 进阶
    • protobuf 编码原理
  • Docker

    • Docker基础
    • Docker常用命令
    • Dockerfile
    • Docker-Compose
    • Docker多阶段构建
    • Docker Config 教程
    • Docker Swarm 教程
    • Docker Stack 教程
    • Docker Buildx 教程
  • k8s

    • k8s 基础概念
    • k8s 集群架构
    • k8s 工作负载
    • Pod 网络
    • Service 网络
    • 外部接入网络
    • 一张图搞懂k8s各种pod
    • k8s 存储抽象
    • mac快速启动k8s
    • 自制申威架构k8s-reloader
  • go-kit

    • go-kit初识
    • go-kit启动http服务
    • go-kit集成gin启动服务
    • go-kit集成grpc和protobuf
    • go-kit中间件
    • go-kit服务注册发现与负载均衡
    • go-kit限流和熔断
    • go-kit链路追踪
    • go-kit集成Prometheus
  • 设计模式

    • 初识设计模式
    • 创建型模式
    • 结构型模式
    • 行为模式
  • 数据结构

    • 时间轮
    • 堆、双向链表、环形队列
    • 队列:优先队列
    • 队列:延迟队列
  • 算法

    • 递归算法
    • 枚举算法
    • 动态规划
    • 回溯算法
    • 分治算法
    • 贪心算法
    • LRU和LFU
    • 一致性哈希
  • go语言基础

  • go语言进阶

  • go语言实现原理

  • gin框架

  • gorm

  • go测试

    • 单元测试
    • benchmark基准测试
      • 简单示例
      • benchmark 是如何工作的
      • 提升准确度
      • 内存分配情况
      • 测试不同的输入
    • pprof 性能分析
  • Go语言
  • go测试
bigox
2022-06-28
目录

benchmark基准测试

# 简单示例

  • fib.go

    package main
    
    func fib(n int) int {
    	if n == 0 || n == 1 {
    		return n
    	}
    	return fib(n-2) + fib(n-1)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
  • fib_test.go

    // fib_test.go
    package main
    
    import "testing"
    
    func BenchmarkFib(b *testing.B) {
    	for n := 0; n < b.N; n++ {
    		fib(30) // run fib(30) b.N times
    	}
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • benchmark 和普通的单元测试用例一样,都位于 _test.go 文件中。

  • 函数名以 Benchmark 开头,参数是 b *testing.B。和普通的单元测试用例很像,单元测试函数名以 Test 开头,参数是 t *testing.T。

  • 运行go test -bench='Fib$'

# benchmark 是如何工作的

  • benchmark 用例的参数 b *testing.B,有个属性 b.N 表示这个用例需要运行的次数。b.N 对于每个用例都是不一样的。

  • 那这个值是如何决定的呢?b.N 从 1 开始,如果该用例能够在 1s 内完成,b.N 的值便会增加,再次执行。b.N 的值大概以 1, 2, 3, 5, 10, 20, 30, 50, 100 这样的序列递增,越到后面,增加得越快。我们仔细观察上述例子的输出:

    BenchmarkFib-8               202           5980669 ns/op
    
    1
  • BenchmarkFib-8 中的 -8 即 GOMAXPROCS,默认等于 CPU 核数。可以通过 -cpu 参数改变 GOMAXPROCS,-cpu 支持传入一个列表作为参数,例如:

    $ go test -bench='Fib$' -cpu=2,4 .
    goos: darwin
    goarch: amd64
    pkg: example
    BenchmarkFib-2               206           5774888 ns/op
    BenchmarkFib-4               205           5799426 ns/op
    PASS
    ok      example 3.563s
    
    1
    2
    3
    4
    5
    6
    7
    8
  • 在这个例子中,改变 CPU 的核数对结果几乎没有影响,因为这个 Fib 的调用是串行的。

  • 202 和 5980669 ns/op 表示用例执行了 202 次,每次花费约 0.006s。总耗时比 1s 略多。

# 提升准确度

  • 对于性能测试来说,提升测试准确度的一个重要手段就是增加测试的次数。我们可以使用 -benchtime和 -count 两个参数达到这个目的。

  • benchmark 的默认时间是 1s,那么我们可以使用 -benchtime 指定为 5s。例如:

    $ go test -bench='Fib$' -benchtime=5s .
    goos: darwin
    goarch: amd64
    pkg: example
    BenchmarkFib-8              1033           5769818 ns/op
    PASS
    ok      example 6.554s
    
    1
    2
    3
    4
    5
    6
    7

    实际执行的时间是 6.5s,比 benchtime 的 5s 要长,测试用例编译、执行、销毁等是需要时间的。

  • 将 -benchtime 设置为 5s,用例执行次数也变成了原来的 5倍,每次函数调用时间仍为 0.6s,几乎没有变化。

  • -benchtime 的值除了是时间外,还可以是具体的次数。例如,执行 30 次可以用 -benchtime=30x:

    $ go test -bench='Fib$' -benchtime=50x .
    goos: darwin
    goarch: amd64
    pkg: example
    BenchmarkFib-8                50           6121066 ns/op
    PASS
    ok      example 0.319s
    
    1
    2
    3
    4
    5
    6
    7
  • 调用 50 次 fib(30),仅花费了 0.319s。

  • -count 参数可以用来设置 benchmark 的轮数。例如,进行 3 轮 benchmark。

    $ go test -bench='Fib$' -benchtime=5s -count=3 .
    goos: darwin
    goarch: amd64
    pkg: example
    BenchmarkFib-8               975           5946624 ns/op
    BenchmarkFib-8              1023           5820582 ns/op
    BenchmarkFib-8               961           6096816 ns/op
    PASS
    ok      example 19.463s
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

# 内存分配情况

  • -benchmem 参数可以度量内存分配的次数。内存分配次数也性能也是息息相关的,例如不合理的切片容量,将导致内存重新分配,带来不必要的开销。

  • 在下面的例子中,generateWithCap 和 generate 的作用是一致的,生成一组长度为 n 的随机序列。唯一的不同在于,generateWithCap 创建切片时,将切片的容量(capacity)设置为 n,这样切片就会一次性申请 n 个整数所需的内存。

    // generate_test.go
    package main
    
    import (
      "math/rand"
      "testing"
      "time"
    )
    
    func generateWithCap(n int) []int {
      rand.Seed(time.Now().UnixNano())
      nums := make([]int, 0, n)
      for i := 0; i < n; i++ {
        nums = append(nums, rand.Int())
      }
      return nums
    }
    
    func generate(n int) []int {
      rand.Seed(time.Now().UnixNano())
      nums := make([]int, 0)
      for i := 0; i < n; i++ {
        nums = append(nums, rand.Int())
      }
      return nums
    }
    
    func BenchmarkGenerateWithCap(b *testing.B) {
      for n := 0; n < b.N; n++ {
        generateWithCap(1000000)
      }
    }
    
    func BenchmarkGenerate(b *testing.B) {
      for n := 0; n < b.N; n++ {
        generate(1000000)
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
  • 运行该用例的结果是:

    $ go test -bench='Generate' .
    goos: darwin
    goarch: amd64
    pkg: example
    BenchmarkGenerateWithCap-8            44          24294582 ns/op
    BenchmarkGenerate-8                   34          30342763 ns/op
    PASS
    ok      example 2.171s
    
    1
    2
    3
    4
    5
    6
    7
    8
  • 可以看到生成 100w 个数字的随机序列,GenerateWithCap 的耗时比 Generate 少 20%。

  • 我们可以使用 -benchmem 参数看到内存分配的情况:

    goos: darwin
    goarch: amd64
    pkg: example
    BenchmarkGenerateWithCap-8  43  24335658 ns/op  8003641 B/op    1 allocs/op
    BenchmarkGenerate-8         33  30403687 ns/op  45188395 B/op  40 allocs/op
    PASS
    ok      example 2.121s
    
    1
    2
    3
    4
    5
    6
    7
  • Generate 分配的内存是 GenerateWithCap 的 6 倍,设置了切片容量,内存只分配一次,而不设置切片容量,内存分配了 40 次。

# 测试不同的输入

  • 不同的函数复杂度不同,O(1),O(n),O(n^2) 等,利用 benchmark 验证复杂度一个简单的方式,是构造不同的输入。对刚才的 benchmark 稍作改造,便能够达到目的。

    // generate_test.go
    package main
    
    import (
      "math/rand"
      "testing"
      "time"
    )
    
    func generate(n int) []int {
      rand.Seed(time.Now().UnixNano())
      nums := make([]int, 0)
      for i := 0; i < n; i++ {
        nums = append(nums, rand.Int())
      }
      return nums
    }
    func benchmarkGenerate(i int, b *testing.B) {
      for n := 0; n < b.N; n++ {
        generate(i)
      }
    }
    
    func BenchmarkGenerate1000(b *testing.B)    { benchmarkGenerate(1000, b) }
    func BenchmarkGenerate10000(b *testing.B)   { benchmarkGenerate(10000, b) }
    func BenchmarkGenerate100000(b *testing.B)  { benchmarkGenerate(100000, b) }
    func BenchmarkGenerate1000000(b *testing.B) { benchmarkGenerate(1000000, b) }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
  • 这里,我们实现一个辅助函数 benchmarkGenerate 允许传入参数 i,并构造了 4 个不同输入的 benchmark 用例。运行结果如下:

    $ go test -bench .                                                       
    goos: darwin
    goarch: amd64
    pkg: example
    BenchmarkGenerate1000-8            34048             34643 ns/op
    BenchmarkGenerate10000-8            4070            295642 ns/op
    BenchmarkGenerate100000-8            403           3230415 ns/op
    BenchmarkGenerate1000000-8            39          32083701 ns/op
    PASS
    ok      example 6.597s
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • 通过测试结果可以发现,输入变为原来的 10 倍,函数每次调用的时长也差不多是原来的 10 倍,这说明复杂度是线性的。

#Go#
上次更新: 2023/04/16, 18:35:33
单元测试
pprof 性能分析

← 单元测试 pprof 性能分析→

最近更新
01
go与http代理
05-24
02
自制申威架构k8s-reloader
12-06
03
Docker Buildx 教程
12-01
更多文章>
Theme by Vdoing | Copyright © 2020-2024 小刘扎扎 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式