首先我们创建目录名为popcount,在popcount目录下创建名为popcount.go的Go文件。我们将文件所属的package命名为popcount。init函数是对package内部的变量进行初始化。我们构建了四个版本的PopCount,然后对四个版本的函数分别进行性能测试,比较运行时间的差异。
package popcount
var pc [256]byte
func init(){
for i := range pc {
pc[i] = pc[i / 2] + byte(i&1)
}
}
func PopCount(x uint64) uint64 {
return uint64(pc[byte(x>>(0*8))] +
pc[byte(x>>(1*8))] +
pc[byte(x>>(2*8))] +
pc[byte(x>>(3*8))] +
pc[byte(x>>(4*8))] +
pc[byte(x>>(5*8))] +
pc[byte(x>>(6*8))] +
pc[byte(x>>(7*8))])
}
func PopCount2(x uint64) uint64 {
var res uint64
for i := 0; i < 64; i+=8 {
res += uint64(pc[byte(x>>i)])
}
return res
}
func PopCount3(x uint64) uint64 {
var res uint64
for i := 0; i < 64; i++ {
res += (x&1)
}
return res
}
func PopCount4(x uint64) uint64 {
var res uint64
for x != 0 {
res += 1
x = x & (x - 1)
}
return res
}
pc数组一共有256个元素,分别表示0-255各个数字所包含的非零比特个数。第一个版本的PopCount函数是对64位的非零比特个数直接进行加和。第二个版本是用for循环和预先计算好的pc数组进行加和。第三个版本是利用for循环遍历了64个比特位,计算非零比特数。第四个版本是利用了位运算,每次消去最右边一个非零比特位。这里只包含判断条件的for循环实际上等同于while循环。
然后我们在popcount目录下创建名为popcount_test.go的测试文件。这里测试文件的名字必须符合如下格式“任意合法名_test.go”。接着,我们将性能测试文件的package命名为popcount,和被测试文件一致,导入testing包,编写四个性能测试函数分别测试前述四个函数。性能测试函数的函数名以Benchmark这个单词开头。
package popcount
import "testing"
func BenchmarkPopCount(b *testing.B) {
for i := 0; i < b.N; i++ {
PopCount(0xfedcba9876543210)
}
}
func BenchmarkPopCount2(b *testing.B) {
for i := 0; i < b.N; i++ {
PopCount2(0xfedcba9876543210)
}
}
func BenchmarkPopCount3(b *testing.B) {
for i := 0; i < b.N; i++ {
PopCount3(0xfedcba9876543210)
}
}
func BenchmarkPopCount4(b *testing.B) {
for i := 0; i < b.N; i++ {
PopCount4(0xfedcba9876543210)
}
}
这里b *testing.B 提供了性能测试所需的几个参数,包括b.N这个整数。
最后我们在该文件夹的命令行中运行
go test -bench=.
-bench后面的字符“.”表示运行所有的测试函数。
稍等,我们就可以得到这样的结果
\golang\ch2\popcount>go test -bench=.
goos: windows
goarch: amd64
BenchmarkPopCount-8 1000000000 0.300 ns/op
BenchmarkPopCount2-8 181161799 6.65 ns/op
BenchmarkPopCount3-8 30758346 38.8 ns/op
BenchmarkPopCount4-8 57693139 20.2 ns/op
PASS
ok /golang/ch2/popcount 5.906s
上面的结果来自amd64指令集下的Windows系统。每个测试函数名后面的8来自GOMAXPROCS,它指定最大逻辑CPU数量。这与并行性能测试息息相关,但是我们这次测试不重要。每个测试函数名后面的第一个数字表示运行次数。第一个测试函数运行了1,000,000,000次,其他函数类似。第三列表示每次运行的耗时,第一个函数每次运行耗时0.3ns,其他函数类似。总共测试时间是5.906s。
测试结果表明(预计算的数组+直接加和的性能)>(预计算的数组+for循环的性能)>(while循环每个非零比特位的性能)>(for循环每一位的性能)。这恰好和“凡事预测立,不预则废”(《礼记》)相符。