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 01:56
最后编辑:admin  更新时间:2024-10-22 02:11