|
| 1 | +Utilizing the Go 1.7 SSA Compiler |
| 2 | + |
| 3 | +Copenhagen Gophers Meetup |
| 4 | +20 Sep 2016 |
| 5 | + |
| 6 | +Klaus Post |
| 7 | +Vivino, Senior Backend Engineer |
| 8 | + |
| 9 | + |
| 10 | + |
| 11 | +* Go SSA Compiler |
| 12 | + |
| 13 | +Main feature of the compiler in Go 1.7 is a "Static Single Assignment" compiler. |
| 14 | + |
| 15 | +Used by some of our favorite tools: |
| 16 | + |
| 17 | +- go vet |
| 18 | +- gorename |
| 19 | +- Go Guru (former Go Oracle) |
| 20 | +- safesql |
| 21 | + |
| 22 | +NOW: |
| 23 | + |
| 24 | +- go build |
| 25 | + |
| 26 | +* What does SSA do? |
| 27 | + |
| 28 | +(stolen from: [[https://talks.golang.org/2014/static-analysis.slide#10][Static analysis tools talks]] by Alan Donovan) |
| 29 | + |
| 30 | +Extremely simplified: |
| 31 | + |
| 32 | +Programs are lowered into Static Single-Assignment form (SSA): |
| 33 | + |
| 34 | +- simplifies dataflow analyses since _reaching_ _definitions_ are implicit |
| 35 | +- invented 1991, now mainstream (gcc, llvm) |
| 36 | + |
| 37 | +All Go programs can be expressed using only ~30 basic instructions |
| 38 | + |
| 39 | +Simple, explicit, high-level, high source fidelity |
| 40 | + |
| 41 | +.link http://godoc.org/golang.org/x/tools/go/ssa golang.org/x/tools/go/ssa |
| 42 | + |
| 43 | + |
| 44 | +* SSA Example |
| 45 | + |
| 46 | + func fib(x int) int { |
| 47 | + if x < 2 { |
| 48 | + return x |
| 49 | + } |
| 50 | + return fib(x-1) + fib(x-2) |
| 51 | + } |
| 52 | + |
| 53 | +↓ |
| 54 | + |
| 55 | + func fib(x int) int: |
| 56 | + 0: entry P:0 S:2 |
| 57 | + t0 = x < 2:int bool |
| 58 | + if t0 goto 1 else 2 |
| 59 | + 1: if.then P:1 S:0 |
| 60 | + return x |
| 61 | + 2: if.done P:1 S:0 |
| 62 | + t1 = x - 1:int int |
| 63 | + t2 = fib(t1) int |
| 64 | + t3 = x - 2:int int |
| 65 | + t4 = fib(t3) int |
| 66 | + t5 = t2 + t4 int |
| 67 | + return t5 |
| 68 | + |
| 69 | +* More Information on SSA |
| 70 | + |
| 71 | +Go Static Analysis Tools by Alan Donovan |
| 72 | + |
| 73 | +.image go17-compiler/donovan-talk.png |
| 74 | + |
| 75 | +.link http://vimeo.com/114736889 Watch the talk on Vimeo |
| 76 | + |
| 77 | +.link https://talks.golang.org/2014/static-analysis.slide#1 Slides |
| 78 | + |
| 79 | +* What does it mean? |
| 80 | + |
| 81 | +- Better code generation |
| 82 | +- Easy optimizations |
| 83 | +- Cross platform optimizations |
| 84 | + |
| 85 | += Faster Go programs. AMD64 for now, others already shaping up. |
| 86 | + |
| 87 | +.image go17-compiler/pixel-gopher-256.png |
| 88 | + |
| 89 | +* SSA "rules" |
| 90 | + |
| 91 | +SSA rules allows to specify optimizations in a relatively simple syntax which is converted into code. |
| 92 | + |
| 93 | +Example: "b = a * -1" |
| 94 | + |
| 95 | + // Convert x * -1 to -x. The front-end catches some but not all of these. |
| 96 | + (Mul8 (Const8 [-1]) x) -> (Neg8 x) |
| 97 | + (Mul16 (Const16 [-1]) x) -> (Neg16 x) |
| 98 | + |
| 99 | +.link https://github.com/golang/go/blob/master/src/cmd/compile/internal/ssa/rewritegeneric.go#L5760 Generated Code |
| 100 | + |
| 101 | +.link https://github.com/golang/go/tree/master/src/cmd/compile/internal/ssa/gen Current SSA Rewrite Rules |
| 102 | + |
| 103 | +* Bound Check Elimination |
| 104 | + |
| 105 | +GC and Bounds Checks are the reason for most of the performance difference between C and Go. |
| 106 | + |
| 107 | +SSA only helps GC to a smaller degree through "escape analysis". |
| 108 | + |
| 109 | +However, SSA helps eliminating bounds checks, by having an easier path for tracing values. |
| 110 | + |
| 111 | +Go 1.7 is now eliminating way more bounds checks, since it is easier to analyze program flow. |
| 112 | + |
| 113 | +* Bounds Check Elimination Example |
| 114 | + |
| 115 | + |
| 116 | +Simple, by type: |
| 117 | + |
| 118 | + var a [256]int |
| 119 | + |
| 120 | + // byte can never be > 255 |
| 121 | + for i := 0; i < 50000; i++ { |
| 122 | + _ = a[byte(i)] |
| 123 | + } |
| 124 | + |
| 125 | + |
| 126 | +By masking lookup: |
| 127 | + |
| 128 | + var b [4096]int |
| 129 | + |
| 130 | + // Mask 11 lower bits |
| 131 | + for i := 0; i < 50000; i++ { |
| 132 | + _ = b[i&4095] |
| 133 | + } |
| 134 | + |
| 135 | + |
| 136 | + |
| 137 | +* Bounds Check Elimination Example |
| 138 | + |
| 139 | +What about slices and not arrays? |
| 140 | + |
| 141 | +Help the compiler! |
| 142 | + |
| 143 | +Example from compress/flate: |
| 144 | + |
| 145 | + // matchLen returns the number of matching bytes in a and b |
| 146 | + // up to length 'max'. Both slices must be at least 'max' |
| 147 | + // bytes in size. |
| 148 | + func matchLen(a, b []byte, max int) int { |
| 149 | + a = a[:max] |
| 150 | + b = b[:len(a)] |
| 151 | + for i, av := range a { |
| 152 | + if b[i] != av { |
| 153 | + return i |
| 154 | + } |
| 155 | + } |
| 156 | + return max |
| 157 | + } |
| 158 | + |
| 159 | +* Identifying Bounds Checks |
| 160 | + |
| 161 | +.code go17-compiler/bounds.go |
| 162 | + |
| 163 | +* Make the compiler tell us |
| 164 | + |
| 165 | +.code go17-compiler/bounds_hl.go |
| 166 | + |
| 167 | + $ go build -gcflags="-d=ssa/check_bce/debug=1" bounds.go |
| 168 | + # command-line-arguments |
| 169 | + .\bounds.go:7: Found IsInBounds |
| 170 | + |
| 171 | +Signifies one or more bounds check in the loop. |
| 172 | + |
| 173 | +* Let's fix it |
| 174 | + |
| 175 | +.code go17-compiler/bounds_fix.go |
| 176 | + |
| 177 | + $ go build -gcflags="-d=ssa/check_bce/debug=1" bounds.go |
| 178 | + # command-line-arguments |
| 179 | + .\bounds.go:5: Found IsSliceInBounds |
| 180 | + .\bounds.go:8: Found IsInBounds |
| 181 | + |
| 182 | +Hmmm... Have we just made it worse? |
| 183 | + |
| 184 | +Guess Go isn't perfect (yet). |
| 185 | + |
| 186 | +* Let's fix it (again) |
| 187 | + |
| 188 | +.code go17-compiler/bounds_fixed.go |
| 189 | + |
| 190 | + $ go build -gcflags="-d=ssa/check_bce/debug=1" bounds.go |
| 191 | + # command-line-arguments |
| 192 | + .\bounds.go:5: Found IsSliceInBounds |
| 193 | + .\bounds.go:7: Found IsSliceInBounds |
| 194 | + |
| 195 | +Success - we now only check bounds outside the loop. |
| 196 | + |
| 197 | + |
| 198 | +* Real world performance |
| 199 | + |
| 200 | +From [[https://github.com/klauspost/dedup][dedup package]]: |
| 201 | + |
| 202 | + var c1 byte // last byte |
| 203 | + var h uint32 // rolling hash for finding fragment boundaries |
| 204 | + var o1 [256]byte // Order 1 prediction table |
| 205 | + |
| 206 | + // Split b based on order 1 predictions. |
| 207 | + func Split(b []byte) int |
| 208 | + for i, c := range b { |
| 209 | + if c == o1[c1] { |
| 210 | + h = (h + uint32(c) + 1) * 314159265 |
| 211 | + } else { |
| 212 | + h = (h + uint32(c) + 1) * 271828182 |
| 213 | + } |
| 214 | + o1[c1] = c |
| 215 | + c1 = c |
| 216 | + if condition { return i } |
| 217 | + } |
| 218 | + } |
| 219 | + |
| 220 | +* Numbers |
| 221 | + |
| 222 | +https://github.com/klauspost/dedup |
| 223 | + |
| 224 | + $ go test -bench=DynamicFragments |
| 225 | + |
| 226 | +Go 1.6.3: |
| 227 | + |
| 228 | + BenchmarkDynamicFragments64K-8 20 59823050 ns/op 175.28 MB/s |
| 229 | + |
| 230 | +Go 1.7.1: |
| 231 | + |
| 232 | + BenchmarkDynamicFragments64K-8 30 37100610 ns/op 282.63 MB/s |
| 233 | + |
| 234 | +* "Mind == Blown" bonus |
| 235 | + |
| 236 | + |
| 237 | + y := x[1] |
| 238 | + z := x[0] |
| 239 | + |
| 240 | +faster than |
| 241 | + |
| 242 | + y := x[0] |
| 243 | + z := x[1] |
| 244 | + |
| 245 | +.image go17-compiler/brad-gopher.jpg |
| 246 | +.caption Brad Fitzpatrick: [[https://twitter.com/bradfitz/status/335213285815226369][Evil, possessed gopher]]. |
| 247 | + |
| 248 | +* Conclusion |
| 249 | + |
| 250 | +- Help the compiler in time critical loops |
| 251 | +- Enjoy the free speedups |
| 252 | +- Check and Benchmark your code |
| 253 | + |
| 254 | +.image go17-compiler/pixel-gopher-256.png |
| 255 | + |
| 256 | +* Questions |
| 257 | + |
| 258 | +.image go17-compiler/questions.png |
| 259 | + |
| 260 | +Feel free to ask questions, or ask later. |
| 261 | + |
| 262 | + |
0 commit comments