github.com/grailbio/base@v0.0.11/simd/multi_benchmark_test.go (about) 1 // Copyright 2019 GRAIL, Inc. All rights reserved. 2 // Use of this source code is governed by the Apache-2.0 3 // license that can be found in the LICENSE file. 4 5 package simd_test 6 7 import ( 8 "runtime" 9 "testing" 10 11 "github.com/grailbio/base/simd" 12 "github.com/grailbio/base/traverse" 13 ) 14 15 // Utility functions to assist with benchmarking of embarrassingly parallel 16 // jobs. It probably makes sense to move this code to a more central location 17 // at some point. 18 19 type multiBenchFunc func(dst, src []byte, nIter int) int 20 21 type taggedMultiBenchFunc struct { 22 f multiBenchFunc 23 tag string 24 } 25 26 type bytesInitFunc func(src []byte) 27 28 type multiBenchmarkOpts struct { 29 dstInit bytesInitFunc 30 srcInit bytesInitFunc 31 } 32 33 func multiBenchmark(bf multiBenchFunc, benchmarkSubtype string, nDstByte, nSrcByte, nJob int, b *testing.B, opts ...multiBenchmarkOpts) { 34 // 'bf' is expected to execute the benchmarking target nIter times. 35 // 36 // Given that, for each of the 3 nCpu settings below, multiBenchmark launches 37 // 'parallelism' goroutines, where each goroutine has nIter set to roughly 38 // (nJob / nCpu), so that the total number of benchmark-target-function 39 // invocations across all threads is nJob. It is designed to measure how 40 // effective traverse.Each-style parallelization is at reducing wall-clock 41 // runtime. 42 totalCpu := runtime.NumCPU() 43 cases := []struct { 44 nCpu int 45 descrip string 46 }{ 47 { 48 nCpu: 1, 49 descrip: "1Cpu", 50 }, 51 // 'Half' is often the saturation point, due to hyperthreading. 52 { 53 nCpu: (totalCpu + 1) / 2, 54 descrip: "HalfCpu", 55 }, 56 { 57 nCpu: totalCpu, 58 descrip: "AllCpu", 59 }, 60 } 61 var dstInit bytesInitFunc 62 var srcInit bytesInitFunc 63 if len(opts) >= 1 { 64 dstInit = opts[0].dstInit 65 srcInit = opts[0].srcInit 66 } 67 for _, c := range cases { 68 success := b.Run(benchmarkSubtype+c.descrip, func(b *testing.B) { 69 dsts := make([][]byte, c.nCpu) 70 srcs := make([][]byte, c.nCpu) 71 for i := 0; i < c.nCpu; i++ { 72 // Add 63 to prevent false sharing. 73 newArrDst := simd.MakeUnsafe(nDstByte + 63) 74 newArrSrc := simd.MakeUnsafe(nSrcByte + 63) 75 if i == 0 { 76 if dstInit != nil { 77 dstInit(newArrDst) 78 } 79 if srcInit != nil { 80 srcInit(newArrSrc) 81 } else { 82 for j := 0; j < nSrcByte; j++ { 83 newArrSrc[j] = byte(j * 3) 84 } 85 } 86 } else { 87 if dstInit != nil { 88 copy(newArrDst[:nDstByte], dsts[0]) 89 } 90 copy(newArrSrc[:nSrcByte], srcs[0]) 91 } 92 dsts[i] = newArrDst[:nDstByte] 93 srcs[i] = newArrSrc[:nSrcByte] 94 } 95 b.ResetTimer() 96 for i := 0; i < b.N; i++ { 97 // May want to replace this with something based on testing.B's 98 // RunParallel method. (Haven't done so yet since I don't see a clean 99 // way to make that play well with per-core preallocated buffers.) 100 _ = traverse.Each(c.nCpu, func(threadIdx int) error { 101 nIter := (((threadIdx + 1) * nJob) / c.nCpu) - ((threadIdx * nJob) / c.nCpu) 102 _ = bf(dsts[threadIdx], srcs[threadIdx], nIter) 103 return nil 104 }) 105 } 106 }) 107 if !success { 108 panic("benchmark failed") 109 } 110 } 111 } 112 113 func bytesInit0(src []byte) { 114 // do nothing 115 } 116 117 func bytesInitMax15(src []byte) { 118 for i := 0; i < len(src); i++ { 119 src[i] = byte(i*3) & 15 120 } 121 } 122 123 type multiBenchVarargsFunc func(args interface{}, nIter int) int 124 125 type taggedMultiBenchVarargsFunc struct { 126 f multiBenchVarargsFunc 127 tag string 128 } 129 130 type varargsFactory func() interface{} 131 132 func multiBenchmarkVarargs(bvf multiBenchVarargsFunc, benchmarkSubtype string, nJob int, argsFactory varargsFactory, b *testing.B) { 133 totalCpu := runtime.NumCPU() 134 cases := []struct { 135 nCpu int 136 descrip string 137 }{ 138 { 139 nCpu: 1, 140 descrip: "1Cpu", 141 }, 142 { 143 nCpu: (totalCpu + 1) / 2, 144 descrip: "HalfCpu", 145 }, 146 { 147 nCpu: totalCpu, 148 descrip: "AllCpu", 149 }, 150 } 151 for _, c := range cases { 152 success := b.Run(benchmarkSubtype+c.descrip, func(b *testing.B) { 153 var argSlice []interface{} 154 for i := 0; i < c.nCpu; i++ { 155 // Can take an "args interface{}" parameter and make deep copies 156 // instead. 157 argSlice = append(argSlice, argsFactory()) 158 } 159 b.ResetTimer() 160 for i := 0; i < b.N; i++ { 161 _ = traverse.Each(c.nCpu, func(threadIdx int) error { 162 nIter := (((threadIdx + 1) * nJob) / c.nCpu) - ((threadIdx * nJob) / c.nCpu) 163 _ = bvf(argSlice[threadIdx], nIter) 164 return nil 165 }) 166 } 167 }) 168 if !success { 169 panic("benchmark failed") 170 } 171 } 172 }