I am curious whether it makes sense to use microoptimizations like
a / 2
versus a >> 1
when a is an integera * 2
vs a << 1
a % 2
vs a & 1
I know that any decent C compiler is good enough handle this. Also please do not write about premature optimization, because these techniques are so obvious, that it is not even optimization and more like a matter of preferences how to write code.
P.S. I tried to do benchmarks and the difference in timing is not statistically significant. I do not know how to check go's bytecode so thank you for pointing it.
Short answer, yes, the compiler optimises those. But it does so slightly differently for int
vs uint
(and presumably any signed vs unsigned integer types such as byte
).
In both cases multiplication and division instructions are avoided but it's only a single instruction for unsigned integers (and a small number of instructions for signed integers). That's because your pairs of statments are only exactly equivalent for unsigned integers and not for signed integers.
Longer answer:
Taking a simple program like:
package main
func main() {}
func div2(a int) {
b := a / 2
c := a >> 1
_, _ = b, c
}
func mul2(a int) {
b := a * 2
c := a << 1
_, _ = b, c
}
func mod2(a int) {
b := a % 2
c := a & 1
_, _ = b, c
}
and running go build -gcflags="-S"
will give you assembly output such as:
"".mod2 t=1 size=32 value=0 args=0x8 locals=0x0
0x0000 00000 (…/opt.go:17) TEXT "".mod2+0(SB),4,$0-8
…
0x0000 00000 (…/opt.go:17) MOVQ "".a+8(FP),BX
…
0x0005 00005 (…/opt.go:18) MOVQ BX,AX
0x0008 00008 (…/opt.go:18) SARQ $63,AX
0x000c 00012 (…/opt.go:18) MOVQ BX,DX
0x000f 00015 (…/opt.go:18) SUBQ AX,DX
0x0012 00018 (…/opt.go:18) ANDQ $1,DX
0x0016 00022 (…/opt.go:18) ADDQ AX,DX
0x0019 00025 (…/opt.go:19) ANDQ $1,BX
0x001d 00029 (…/opt.go:21) RET ,
Here BX
is the argument and DX
and BX
appear to be the two results (BX
being reused as one of the results). Here they are slightly different, but only by a few instructions (look at the source line numbers shown) and without any division or multiplication instructions (so basically just as fast). The difference is due to algorithmic vs logical shifts and how Go does mod for negative values.
You can confirm this by changing int
to uint
in the program and then the output contains things like:
0x0008 00008 (…/opt.go:18) ANDQ $1,CX
0x000c 00012 (…/opt.go:19) ANDQ $1,BX
i.e. the exact same instruction. This is true for each of the examples you gave.