Test 是Go Test概念中的⼀种测试,我们把它称之为单元测试或者叫做测试。Go还提供了其它三种类型:
- benchmark:性能测试
- example: 示例
- fuzz: 模糊测试
这⼀章,我们重点介绍benchmark测试。benchmark测试主要⽤来测试函数和⽅法的性能。Go标准库中有很多的benchmark的代码,以便验证Go运⾏时和标准库的性能。如果你给Go标准库提供⼀个性能优化的代码,你⼀定要运⾏相关的benchmark或者补充⼀系列场景下的benchmark。
⽐如字节跳动语⾔团队在Go 1.19提交了⼀个Sort⽅法的性能优化,就提供了⼤量的排序场景,以便验证新的算法⽐先前的算法确实有优化:
name old time/op new time/op delta
Example-8 3.60µs ± 0% 0.17µs ± 1% -95.29% (p=0.008 n=5+5)
name old alloc/op new alloc/op delta
Example-8 32.0B ± 0% 8.0B ± 0% -75.00% (p=0.008 n=5+5)
name old allocs/op new allocs/op delta
Example-8 1.00 ± 0% 1.00 ± 0% ~ (all equal)
benchmark的函数依然放在以 _test.go
为后缀的⽂件中,你可以和包含单元测试函数的⽂件分开,也可以和包含单元测试的函数写在⼀起。
benchmark函数的签名如下所示:
func BenchmarkXxx(b *testing.B) { ... }
和单元测试的函数签名类似,只不过 Test 换成了 Benchmark ,函数参数变成了 b *testing.B
。
使⽤ b.N
,你可以根据benchmark⾃动调整的运⾏次数退出测试。
下⾯是Benchmark的⼀个例⼦。
package s8
import (
"crypto/md5"
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"fmt"
"hash/crc32"
"hash/fnv"
_ "runtime"
"testing"
"unsafe"
xxhashasm "github.com/cespare/xxhash"
"github.com/creachadair/cityhash"
afarmhash "github.com/dgryski/go-farm"
farmhash "github.com/leemcloughlin/gofarmhash"
"github.com/minio/highwayhash"
"github.com/pierrec/xxHash/xxHash64"
"github.com/spaolacci/murmur3"
)
var n int64
var testBytes []byte
func BenchmarkHash(b *testing.B) {
sizes := []int64{32, 64, 128, 256, 512, 1024}
for _, n = range sizes {
testBytes = make([]byte, n)
readN, err := rand.Read(testBytes)
if readN != int(n) {
panic(fmt.Sprintf("expect %d but got %d", n, readN))
}
if err != nil {
panic(err)
}
b.Run(fmt.Sprintf("Sha1-%d", n), benchmarkSha1)
b.Run(fmt.Sprintf("Sha256-%d", n), benchmarkSha256)
b.Run(fmt.Sprintf("Sha512-%d", n), benchmarkSha512)
b.Run(fmt.Sprintf("MD5-%d", n), benchmarkMD5)
b.Run(fmt.Sprintf("Fnv-%d", n), benchmarkFnv)
b.Run(fmt.Sprintf("Crc32-%d", n), benchmarkCrc32)
b.Run(fmt.Sprintf("CityHash-%d", n), benchmarkCityhash)
b.Run(fmt.Sprintf("FarmHash-%d", n), benchmarkFarmhash)
b.Run(fmt.Sprintf("Farmhash_dgryski-%d", n), benchmarkFarmhash_dgryski)
b.Run(fmt.Sprintf("Murmur3-%d", n), benchmarkMurmur3)
b.Run(fmt.Sprintf("Highwayhash-%d", n), benchmarkHighwayhash)
b.Run(fmt.Sprintf("XXHash64-%d", n), benchmarkXXHash64)
b.Run(fmt.Sprintf("XXHash64_ASM-%d", n), benchmarkXXHash64_ASM)
b.Run(fmt.Sprintf("MapHash64-%d", n), benchmarkMapHash64)
fmt.Println()
}
}
func benchmarkSha1(b *testing.B) {
x := sha1.New()
b.SetBytes(n)
b.ResetTimer()
for i := 0; i < b.N; i++ {
x.Reset()
x.Write(testBytes)
_ = x.Sum(nil)
}
}
func benchmarkSha256(b *testing.B) {
x := sha256.New()
b.SetBytes(n)
b.ResetTimer()
for i := 0; i < b.N; i++ {
x.Reset()
x.Write(testBytes)
_ = x.Sum(nil)
}
}
func benchmarkSha512(b *testing.B) {
x := sha512.New()
b.SetBytes(n)
b.ResetTimer()
for i := 0; i < b.N; i++ {
x.Reset()
x.Write(testBytes)
_ = x.Sum(nil)
}
}
func benchmarkMD5(b *testing.B) {
x := md5.New()
b.SetBytes(n)
b.ResetTimer()
for i := 0; i < b.N; i++ {
x.Reset()
x.Write(testBytes)
_ = x.Sum(nil)
}
}
func benchmarkCrc32(b *testing.B) {
x := crc32.NewIEEE()
b.SetBytes(n)
b.ResetTimer()
for i := 0; i < b.N; i++ {
x.Reset()
x.Write(testBytes)
_ = x.Sum32()
}
}
func benchmarkFnv(b *testing.B) {
x := fnv.New64()
b.SetBytes(n)
b.ResetTimer()
for i := 0; i < b.N; i++ {
x.Reset()
x.Write(testBytes)
_ = x.Sum64()
}
}
func benchmarkCityhash(b *testing.B) {
b.SetBytes(n)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = cityhash.Hash64WithSeed(testBytes, 0xCAFE)
}
}
func benchmarkFarmhash(b *testing.B) {
b.SetBytes(n)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = farmhash.Hash64WithSeed(testBytes, 0xCAFE)
}
}
func benchmarkFarmhash_dgryski(b *testing.B) {
b.SetBytes(n)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = afarmhash.Hash64WithSeed(testBytes, 0xCAFE)
}
}
func benchmarkMurmur3(b *testing.B) {
x := murmur3.New64()
b.SetBytes(n)
b.ResetTimer()
for i := 0; i < b.N; i++ {
x.Reset()
x.Write(testBytes)
_ = x.Sum64()
}
}
func benchmarkHighwayhash(b *testing.B) {
key, _ :=
hex.DecodeString("000102030405060708090A0B0C0D0E0FF0E0D0C0B0A090807060504030201
000") // use your own key here
x, _ := highwayhash.New64(key)
b.SetBytes(n)
b.ResetTimer()
for i := 0; i < b.N; i++ {
x.Reset()
x.Write(testBytes)
_ = x.Sum64()
}
}
func benchmarkXXHash64(b *testing.B) {
x := xxHash64.New(0xCAFE)
b.SetBytes(n)
b.ResetTimer()
for i := 0; i < b.N; i++ {
x.Reset()
x.Write(testBytes)
_ = x.Sum64()
}
}
func benchmarkXXHash64_ASM(b *testing.B) {
x := xxhashasm.New()
b.SetBytes(n)
b.ResetTimer()
for i := 0; i < b.N; i++ {
x.Reset()
x.Write(testBytes)
_ = x.Sum64()
}
}
//go:noescape
//go:linkname memhash runtime.memhash
func memhash(p unsafe.Pointer, h, s uintptr) uintptr
type stringStruct struct {
str unsafe.Pointer
len int
}
func MemHash(data []byte) uint64 {
ss := (*stringStruct)(unsafe.Pointer(&data))
return uint64(memhash(ss.str, 0, uintptr(ss.len)))
}
func benchmarkMapHash64(b *testing.B) {
b.SetBytes(n)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = MemHash(testBytes)
}
}
这段代码摘⾃我的⼀个github 项⽬: smallnest/hash-bench,⽤来⽐较各种Hash算法的性能。
为了测试哈希不同的数据⼤⼩的各种算法的性能,我实现了⼦测试。针对⼏种数据⼤⼩,分别执⾏⼦测试:
64
var testBytes []byte
func BenchmarkHash(b *testing.B) {
sizes := []int64{32, 64, 128, 256, 512, 1024}
for _, n = range sizes {
testBytes = make([]byte, n)
readN, err := rand.Read(testBytes)
if readN != int(n) {
panic(fmt.Sprintf("expect %d but got %d", n, readN))
}
if err != nil {
panic(err)
}
b.Run(fmt.Sprintf("Sha1-%d", n), benchmarkSha1)
b.Run(fmt.Sprintf("Sha256-%d", n), benchmarkSha256)
b.Run(fmt.Sprintf("Sha512-%d", n), benchmarkSha512)
b.Run(fmt.Sprintf("MD5-%d", n), benchmarkMD5)
b.Run(fmt.Sprintf("Fnv-%d", n), benchmarkFnv)
b.Run(fmt.Sprintf("Crc32-%d", n), benchmarkCrc32)
b.Run(fmt.Sprintf("CityHash-%d", n), benchmarkCityhash)
b.Run(fmt.Sprintf("FarmHash-%d", n), benchmarkFarmhash)
b.Run(fmt.Sprintf("Farmhash_dgryski-%d", n), benchmarkFarmhash_dgryski)
b.Run(fmt.Sprintf("Murmur3-%d", n), benchmarkMurmur3)
b.Run(fmt.Sprintf("Highwayhash-%d", n), benchmarkHighwayhash)
b.Run(fmt.Sprintf("XXHash64-%d", n), benchmarkXXHash64)
b.Run(fmt.Sprintf("XXHash64_ASM-%d", n), benchmarkXXHash64_ASM)
b.Run(fmt.Sprintf("MapHash
运⾏ go test -benchmem -bench . -v
:
它会把每⼀个函数按照耗时罗列出来。这⾥我们使⽤了 -benchmem
参数,所以还会把每个op的分配的次数和字节数显示出来。 ⼀般来说内存分配次数越少,分配的字节数越少性能会越⾼,所以它给你指明了⼀个优化的⽅向。当然你在测试代码中调⽤ b.ReportAllocs() 也会报告内存分配信息,即使你没有设置 -benchmem
参数。
在代码中我们还加⼊了 b.SetBytes(n)
,所以结果中还报告了每秒处理的字节数,当然在本测试中处理的越多越好。
-benchtime t
可以设置充⾜的测试次数,直到指定的测试时间。如果不是时间,⽽是倍数的话,指定的是测试次数,如 -benchtime 100x
。
并发benchmark
有时候我们想测试⼀下并发情况下的性能,⽐如对⼀个kv数据结构,我们想了解它在并发读写情况下的性能。这个时候就要⽤到并发benchmark。
⽐如下⾯这段代码有两个benchmark函数,分别测试并发情况下的性能:
package s8_test
import (
"crypto/sha256"
"fmt"
"runtime"
"testing"
)
func init() {
fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(-1))
}
func BenchmarkParallelHash(b *testing.B) {
testBytes := make([]byte, 1024)
b.ResetTimer()
b.SetParallelism(20)
b.RunParallel(func(pb *testing.PB) {
x := sha256.New()
for pb.Next() {
x.Reset()
x.Write(testBytes)
_ = x.Sum(nil)
}
})
}
func BenchmarkNonParallelHash(b *testing.B) {
testBytes := make([]byte, 1024)
b.ResetTimer()
x := sha256.New()
for i := 0; i < b.N; i++ {
x.Reset()
x.Write(testBytes)
_ = x.Sum(nil)
}
}
注意这⾥ xxx ns/op
可能会导致误导。这⾥的时间是墙上的时钟流逝的时间,并不是⼀个op所需的时间,所以你可以看到并发情况下因为有多个goroutine(此图中是6个)并发执⾏,所以总体的时间加快了,接近串⾏执⾏的六分之⼀。
你可以调⽤ b.SetParallelism(p)
调⼤并发度为 p*GOMAXPROCS
同⼀函数优化后的性能⽐较除了同时⽐较多个不同函数的性能,⽐如序列化反序列化,defer函数的影响,不同http touter
的性能,我们还可以⽐较同⼀个函数不同优化后的性能,⽐如前⾯提到的Sort函数的优化。
假设我们有⼀个Hash函数,先前是通过sha256实现的:
func BenchmarkExample(b *testing.B) {
x := sha256.New()
testBytes := make([]byte, 1024)
b.ResetTimer()
for i := 0; i < b.N; i++ {
x.Reset()
x.Write(testBytes)
_ = x.Sum(nil)
}
}
我们运⾏benchmark,测试5次, 将结果写⼊到 old.txt
:
go test -bench BenchmarkExample . -v -count=5 > old.txt
我们换⼀种实现,换成Go 1.19中的maphash实现:
func BenchmarkExample(b *testing.B) {
x := maphash.Hash{}
testBytes := make([]byte, 1024)
b.ResetTimer()
for i := 0; i < b.N; i++ {
x.Reset()
x.Write(testBytes)
_ = x.Sum(nil)
}
}
也执⾏benchmark5次,写⼊到 new.txt 中:
go test -bench BenchmarkExample . -v -count=5 > new.txt
接下来就可以使⽤benchstat进⾏⽐较了。你如果没有安装benchstat命令,那么你可以执⾏ go install golang.org/x/perf/cmd/benchstat`
@latest` 安装先。
我们看第⼀⾏就好了。第⼆⾏和第三⾏是对分配内存和分配次数的⽐较。
可以看到先前的Hash实现美哦个op需要3.60us
左右,⽽新的hash算法值需要0.17us
,减少了95.29%
,说明新的Hash优势巨⼤。
p=0.008
是可信度,越⼩可信度越⾼。统计学中通常把 p=0.05
作为临界值,超过此值说明结果不可信。
后⾯的 n=5+5 说明采⽤了⽼的测试的5个样本,新的测试的5个样本。如果某次测试的数据偏差太⼤,那么会被踢出,样本数相应的减少⼀个。这⾥我们都执⾏的5次测试都可信。执⾏次数多会减少测试的偏差,所以⼀般最好测试20次以上。
最后编辑:admin 更新时间:2024-10-22 02:11